diff --git a/.devcontainer/README.md b/.devcontainer/README.md index de1742c74b1..f31ebb25e2c 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -8,7 +8,7 @@ This repository includes configuration for a development container for working w 1. Install Docker Desktop or Docker for Linux on your local machine. (See [docs](https://aka.ms/vscode-remote/containers/getting-started) for additional details.) -2. **Important**: Docker needs at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run a full build. If you are on macOS, or are using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item and going to **Preferences/Settings > Resources > Advanced**. +2. **Important**: Docker needs at least **4 Cores and 8 GB of RAM** to run a full build. If you are on macOS, or are using the old Hyper-V engine for Windows, update these values for Docker Desktop by right-clicking on the Docker status bar item and going to **Preferences/Settings > Resources > Advanced**. > **Note:** The [Resource Monitor](https://marketplace.visualstudio.com/items?itemName=mutantdino.resourcemonitor) extension is included in the container so you can keep an eye on CPU/Memory in the status bar. diff --git a/.devcontainer/cache/.gitignore b/.devcontainer/cache/.gitignore deleted file mode 100644 index 4f96ddff402..00000000000 --- a/.devcontainer/cache/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.manifest diff --git a/.devcontainer/cache/before-cache.sh b/.devcontainer/cache/before-cache.sh index 9548a154c38..78511d273d1 100755 --- a/.devcontainer/cache/before-cache.sh +++ b/.devcontainer/cache/before-cache.sh @@ -4,12 +4,12 @@ # are run. Its just a find command that filters out a few things we don't need to watch. set -e - -SCRIPT_PATH="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" SOURCE_FOLDER="${1:-"."}" +CACHE_FOLDER="${2:-"$HOME/.devcontainer-cache"}" cd "${SOURCE_FOLDER}" echo "[$(date)] Generating ""before"" manifest..." -find -L . -not -path "*/.git/*" -and -not -path "${SCRIPT_PATH}/*.manifest" -type f > "${SCRIPT_PATH}/before.manifest" +mkdir -p "${CACHE_FOLDER}" +find -L . -not -path "*/.git/*" -and -not -path "${CACHE_FOLDER}/*.manifest" -type f > "${CACHE_FOLDER}/before.manifest" echo "[$(date)] Done!" diff --git a/.devcontainer/cache/build-cache-image.sh b/.devcontainer/cache/build-cache-image.sh index 865b860898c..451d1ab45a9 100755 --- a/.devcontainer/cache/build-cache-image.sh +++ b/.devcontainer/cache/build-cache-image.sh @@ -19,10 +19,10 @@ TAG="branch-${BRANCH//\//-}" echo "[$(date)] ${BRANCH} => ${TAG}" cd "${SCRIPT_PATH}/../.." -echo "[$(date)] Starting image build..." -docker build -t ${CONTAINER_IMAGE_REPOSITORY}:"${TAG}" -f "${SCRIPT_PATH}/cache.Dockerfile" . -echo "[$(date)] Image build complete." +echo "[$(date)] Starting image build and push..." +export DOCKER_BUILDKIT=1 +docker buildx create --use --name vscode-dev-containers +docker run --privileged --rm tonistiigi/binfmt --install all +docker buildx build --push --platform linux/amd64,linux/arm64 -t ${CONTAINER_IMAGE_REPOSITORY}:"${TAG}" -f "${SCRIPT_PATH}/cache.Dockerfile" . -echo "[$(date)] Pushing image..." -docker push ${CONTAINER_IMAGE_REPOSITORY}:"${TAG}" echo "[$(date)] Done!" diff --git a/.devcontainer/cache/cache-diff.sh b/.devcontainer/cache/cache-diff.sh index 3f8b77e5602..c2444b8fc6b 100755 --- a/.devcontainer/cache/cache-diff.sh +++ b/.devcontainer/cache/cache-diff.sh @@ -5,16 +5,19 @@ set -e -SCRIPT_PATH="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" SOURCE_FOLDER="${1:-"."}" -CACHE_FOLDER="${2:-"/usr/local/etc/devcontainer-cache"}" +CACHE_FOLDER="${2:-"$HOME/.devcontainer-cache"}" + +if [ ! -d "${CACHE_FOLDER}" ]; then + echo "No cache folder found. Be sure to run before-cache.sh to set one up." + exit 1 +fi echo "[$(date)] Starting cache operation..." cd "${SOURCE_FOLDER}" echo "[$(date)] Determining diffs..." -find -L . -not -path "*/.git/*" -and -not -path "${SCRIPT_PATH}/*.manifest" -type f > "${SCRIPT_PATH}/after.manifest" -grep -Fxvf "${SCRIPT_PATH}/before.manifest" "${SCRIPT_PATH}/after.manifest" > "${SCRIPT_PATH}/cache.manifest" +find -L . -not -path "*/.git/*" -and -not -path "${CACHE_FOLDER}/*.manifest" -type f > "${CACHE_FOLDER}/after.manifest" +grep -Fxvf "${CACHE_FOLDER}/before.manifest" "${CACHE_FOLDER}/after.manifest" > "${CACHE_FOLDER}/cache.manifest" echo "[$(date)] Archiving diffs..." -mkdir -p "${CACHE_FOLDER}" -tar -cf "${CACHE_FOLDER}/cache.tar" --totals --files-from "${SCRIPT_PATH}/cache.manifest" +tar -cf "${CACHE_FOLDER}/cache.tar" --totals --files-from "${CACHE_FOLDER}/cache.manifest" echo "[$(date)] Done! $(du -h "${CACHE_FOLDER}/cache.tar")" diff --git a/.devcontainer/cache/cache.Dockerfile b/.devcontainer/cache/cache.Dockerfile index a2c2866fe23..217122a4e9b 100644 --- a/.devcontainer/cache/cache.Dockerfile +++ b/.devcontainer/cache/cache.Dockerfile @@ -4,19 +4,21 @@ # This first stage generates cache.tar FROM mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:dev as cache ARG USERNAME=node +ARG CACHE_FOLDER="/home/${USERNAME}/.devcontainer-cache" COPY --chown=${USERNAME}:${USERNAME} . /repo-source-tmp/ -RUN mkdir /usr/local/etc/devcontainer-cache \ - && chown ${USERNAME} /usr/local/etc/devcontainer-cache /repo-source-tmp \ +RUN mkdir -p ${CACHE_FOLDER} && chown ${USERNAME} ${CACHE_FOLDER} /repo-source-tmp \ && su ${USERNAME} -c "\ cd /repo-source-tmp \ - && .devcontainer/cache/before-cache.sh \ - && .devcontainer/prepare.sh \ - && .devcontainer/cache/cache-diff.sh" + && .devcontainer/cache/before-cache.sh . ${CACHE_FOLDER} \ + && .devcontainer/prepare.sh . ${CACHE_FOLDER} \ + && .devcontainer/cache/cache-diff.sh . ${CACHE_FOLDER}" # This second stage starts fresh and just copies in cache.tar from the previous stage. The related # devcontainer.json file is then setup to have postCreateCommand fire restore-diff.sh to expand it. FROM mcr.microsoft.com/vscode/devcontainers/repos/microsoft/vscode:dev as dev-container ARG USERNAME=node -ARG CACHE_FOLDER="/usr/local/etc/devcontainer-cache" -RUN mkdir -p "${CACHE_FOLDER}" && chown "${USERNAME}:${USERNAME}" "${CACHE_FOLDER}" +ARG CACHE_FOLDER="/home/${USERNAME}/.devcontainer-cache" +RUN mkdir -p "${CACHE_FOLDER}" \ + && chown "${USERNAME}:${USERNAME}" "${CACHE_FOLDER}" \ + && su ${USERNAME} -c "git config --global codespaces-theme.hide-status 1" COPY --from=cache ${CACHE_FOLDER}/cache.tar ${CACHE_FOLDER}/ diff --git a/.devcontainer/cache/restore-diff.sh b/.devcontainer/cache/restore-diff.sh index 827afc45ab1..e8ea93f3f35 100755 --- a/.devcontainer/cache/restore-diff.sh +++ b/.devcontainer/cache/restore-diff.sh @@ -5,9 +5,8 @@ # is already up where you would typically run a command like "yarn install". set -e - SOURCE_FOLDER="$(cd "${1:-"."}" && pwd)" -CACHE_FOLDER="${2:-"/usr/local/etc/devcontainer-cache"}" +CACHE_FOLDER="${2:-"$HOME/.devcontainer-cache"}" if [ ! -d "${CACHE_FOLDER}" ]; then echo "No cache folder found." @@ -16,7 +15,15 @@ fi echo "[$(date)] Expanding $(du -h "${CACHE_FOLDER}/cache.tar") file to ${SOURCE_FOLDER}..." cd "${SOURCE_FOLDER}" -tar -xf "${CACHE_FOLDER}/cache.tar" -rm -f "${CACHE_FOLDER}/cache.tar" +# Ensure user/group is correct if the UID/GID was changed for some reason +echo "+1000 +$(id -u)" > "${CACHE_FOLDER}/cache-owner-map" +echo "+1000 +$(id -g)" > "${CACHE_FOLDER}/cache-group-map" +# Untar to workspace folder, preserving permissions and order, but mapping GID/UID if required +tar --owner-map="${CACHE_FOLDER}/cache-owner-map" --group-map="${CACHE_FOLDER}/cache-group-map" -xpsf "${CACHE_FOLDER}/cache.tar" +rm -rf "${CACHE_FOLDER}" echo "[$(date)] Done!" +# Change ownership of chrome-sandbox +sudo chown root .build/electron/chrome-sandbox +sudo chmod 4755 .build/electron/chrome-sandbox + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b9a287013f8..3e40ce61f95 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -30,11 +30,11 @@ ], // Optionally loads a cached yarn install for the repo - "postCreateCommand": ".devcontainer/cache/restore-diff.sh && sudo chown node:node /workspaces", + "postCreateCommand": ".devcontainer/cache/restore-diff.sh", "remoteUser": "node", "hostRequirements": { - "memory": "6gb" + "memory": "8gb" } } diff --git a/.eslintrc.json b/.eslintrc.json index 0a6dc4864c3..4f6bbe81b13 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -164,6 +164,7 @@ "delete", "discover", "dispose", + "drop", "edit", "end", "expand", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f191223bee..12973f26771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - uses: actions/setup-python@v2 with: @@ -57,14 +57,7 @@ jobs: env: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 ELECTRON_SKIP_BINARY_DOWNLOAD: 1 - run: | - # update node-gyp to latest for support in detecting VS 2022 toolchain - npm install -g node-gyp@latest - # Resolve to node-gyp.js - # Remove this once node-version > 17.4.x or > 16.14.0, - # which ships with npm > 8.4.0 that has support for VS 2022 toolchain. - $env:npm_config_node_gyp=$(Join-Path $(Get-Command node-gyp.cmd).Path "..\node_modules\node-gyp\bin\node-gyp.js" -Resolve) - yarn --frozen-lockfile --network-timeout 180000 + run: yarn --frozen-lockfile --network-timeout 180000 - name: Create node_modules archive if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} run: | @@ -120,7 +113,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - name: Compute node modules cache key id: nodeModulesCacheKey @@ -130,8 +123,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules20-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules20- + key: ${{ runner.os }}-cacheNodeModules21-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules21- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -192,7 +185,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - name: Compute node modules cache key id: nodeModulesCacheKey @@ -202,8 +195,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules20-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules20- + key: ${{ runner.os }}-cacheNodeModules21-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules21- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -266,7 +259,7 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - name: Compute node modules cache key id: nodeModulesCacheKey @@ -276,8 +269,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules20-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules20- + key: ${{ runner.os }}-cacheNodeModules21-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules21- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -314,43 +307,8 @@ jobs: - name: Run eslint run: yarn eslint - - name: Run Monaco Editor Checks - run: yarn monaco-compile-check - - name: Run vscode-dts Compile Checks run: yarn vscode-dts-compile-check - name: Run Trusted Types Checks run: yarn tsec-compile-check - - - name: Editor Distro & ESM Bundle - run: yarn gulp editor-esm-bundle - - - name: Editor ESM sources check - working-directory: ./test/monaco - run: yarn run esm-check - - - name: Typings validation prep - run: | - mkdir typings-test - - - name: Typings validation - working-directory: ./typings-test - run: | - yarn init -yp - ../node_modules/.bin/tsc --init - echo "import '../out-monaco-editor-core';" > a.ts - ../node_modules/.bin/tsc --noEmit - - - name: Package Editor with Webpack - working-directory: ./test/monaco - run: yarn run bundle-webpack - - - name: Compile Editor Tests - working-directory: ./test/monaco - run: yarn run compile - - - name: Run Editor Tests - timeout-minutes: 5 - working-directory: ./test/monaco - run: yarn test diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml new file mode 100644 index 00000000000..84955eca85c --- /dev/null +++ b/.github/workflows/monaco-editor.yml @@ -0,0 +1,91 @@ +name: Monaco Editor checks + +on: + push: + branches: + - main + - release/* + pull_request: + branches: + - main + - release/* + +jobs: + main: + name: Monaco Editor checks + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-node@v2 + with: + node-version: 14 + + - name: Compute node modules cache key + id: nodeModulesCacheKey + run: echo "::set-output name=value::$(node build/azure-pipelines/common/computeNodeModulesCacheKey.js)" + - name: Cache node modules + id: cacheNodeModules + uses: actions/cache@v2 + with: + path: "**/node_modules" + key: ${{ runner.os }}-cacheNodeModules20-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules20- + - name: Get yarn cache directory path + id: yarnCacheDirPath + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache yarn directory + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + uses: actions/cache@v2 + with: + path: ${{ steps.yarnCacheDirPath.outputs.dir }} + key: ${{ runner.os }}-yarnCacheDir-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-yarnCacheDir- + - name: Execute yarn + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + run: yarn --frozen-lockfile --network-timeout 180000 + + - name: Download Playwright + run: yarn playwright-install + + - name: Run Monaco Editor Checks + run: yarn monaco-compile-check + + - name: Editor Distro & ESM Bundle + run: yarn gulp editor-esm-bundle + + - name: Editor ESM sources check + working-directory: ./test/monaco + run: yarn run esm-check + + - name: Typings validation prep + run: | + mkdir typings-test + + - name: Typings validation + working-directory: ./typings-test + run: | + yarn init -yp + ../node_modules/.bin/tsc --init + echo "import '../out-monaco-editor-core';" > a.ts + ../node_modules/.bin/tsc --noEmit + + - name: Package Editor with Webpack + working-directory: ./test/monaco + run: yarn run bundle-webpack + + - name: Compile Editor Tests + working-directory: ./test/monaco + run: yarn run compile + + - name: Run Editor Tests + timeout-minutes: 5 + working-directory: ./test/monaco + run: yarn test diff --git a/.github/workflows/rich-navigation.yml b/.github/workflows/rich-navigation.yml index bd9b7d1b560..a154e88f61f 100644 --- a/.github/workflows/rich-navigation.yml +++ b/.github/workflows/rich-navigation.yml @@ -24,18 +24,11 @@ jobs: - uses: actions/setup-node@v2 with: - node-version: 14 + node-version: 16 - name: Install dependencies if: steps.caching-stage.outputs.cache-hit != 'true' - run: | - # update node-gyp to latest for support in detecting VS 2022 toolchain - npm install -g node-gyp@latest - # Resolve to node-gyp.js - # Remove this once node-version > 17.4.x or > 16.14.0, - # which ships with npm > 8.4.0 that has support for VS 2022 toolchain. - $env:npm_config_node_gyp=$(Join-Path $(Get-Command node-gyp.cmd).Path "..\node_modules\node-gyp\bin\node-gyp.js" -Resolve) - yarn --frozen-lockfile + run: yarn --frozen-lockfile env: CHILD_CONCURRENCY: 1 diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 459108d2ebf..3380a5c0ca9 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-unpkg\n\n$MILESTONE=milestone:\"February 2022\"" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-unpkg\n\n$MILESTONE=milestone:\"March 2022\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index ce87bb39a35..c8c4d201c75 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal\n\n$MILESTONE=milestone:\"February 2022\"\n\n$MINE=assignee:@me" + "value": "$REPOS=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-js-debug repo:microsoft/vscode-remote-release repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remotehub repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-livepreview repo:microsoft/vscode-python repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal\n\n$MILESTONE=milestone:\"March 2022\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index f3392fc0e43..53dad858d0d 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github\n\n// current milestone name\n$milestone=milestone:\"March 2022\"" + "value": "// list of repos we work in\r\n$repos=repo:microsoft/vscode repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev repo:microsoft/vscode-unpkg repo:microsoft/vscode-references-view repo:microsoft/vscode-anycode repo:microsoft/vscode-hexeditor repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-livepreview repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-remote-repositories-github repo:microsoft/monaco-editor repo:microsoft/vscode-vsce\r\n\r\n// current milestone name\r\n$milestone=milestone:\"March 2022\"" }, { "kind": 1, @@ -92,7 +92,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream -label:polish -label:testplan-item" + "value": "$repos assignee:@me is:open type:issue -label:bug -label:\"needs more info\" -label:feature-request -label:under-discussion -label:debt -label:plan-item -label:upstream -label:polish -label:testplan-item -label:error-telemetry" }, { "kind": 1, @@ -102,7 +102,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"needs more info\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:color-palette -label:comments -label:config -label:context-keys -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:engineering -label:error-list -label:extension-host -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:gpu -label:grammar -label:grid-view -label:html -label:i18n -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-window -label:ipc -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list -label:live-server -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:network -label:notebook -label:notebook-api -label:notebook-celltoolbar -label:notebook-diff -label:notebook-dnd -label:notebook-folding -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-keybinding -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-statusbar -label:open-editors -label:opener -label:outline -label:output -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-explorer -label:remotehub -label:rename -label:sandbox -label:sash -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview -label:suggest -label:sync-error-handling -label:table -label:tasks -label:telemetry -label:terminal -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-links -label:terminal-local-echo -label:terminal-profiles -label:terminal-reconnection -label:terminal-rendering -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typehierarchy -label:typescript -label:undo-redo -label:uri -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-build -label:vscode-website -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom" + "value": "repo:microsoft/vscode assignee:@me is:open type:issue -label:\"needs more info\" -label:api -label:api-finalization -label:api-proposal -label:authentication -label:bisect-ext -label:bracket-pair-colorization -label:bracket-pair-guides -label:breadcrumbs -label:callhierarchy -label:chrome-devtools -label:code-lens -label:color-palette -label:comments -label:config -label:context-keys -label:css-less-scss -label:custom-editors -label:debug -label:debug-disassembly -label:dialogs -label:diff-editor -label:dropdown -label:editor -label:editor-autoclosing -label:editor-autoindent -label:editor-bracket-matching -label:editor-clipboard -label:editor-code-actions -label:editor-color-picker -label:editor-columnselect -label:editor-commands -label:editor-comments -label:editor-contrib -label:editor-core -label:editor-drag-and-drop -label:editor-error-widget -label:editor-find -label:editor-folding -label:editor-highlight -label:editor-hover -label:editor-indent-detection -label:editor-indent-guides -label:editor-input -label:editor-input-IME -label:editor-insets -label:editor-minimap -label:editor-multicursor -label:editor-parameter-hints -label:editor-render-whitespace -label:editor-rendering -label:editor-RTL -label:editor-scrollbar -label:editor-symbols -label:editor-synced-region -label:editor-textbuffer -label:editor-theming -label:editor-wordnav -label:editor-wrapping -label:emmet -label:engineering -label:error-list -label:extension-host -label:extension-recommendations -label:extensions -label:extensions-development -label:file-decorations -label:file-encoding -label:file-explorer -label:file-glob -label:file-io -label:file-watcher -label:font-rendering -label:formatting -label:getting-started -label:ghost-text -label:git -label:github -label:gpu -label:grammar -label:grid-view -label:html -label:i18n -label:icon-brand -label:icons-product -label:image-preview -label:inlay-hints -label:inline-completions -label:install-update -label:intellisense-config -label:interactive-window -label:ipc -label:issue-bot -label:issue-reporter -label:javascript -label:json -label:keybindings -label:keybindings-editor -label:keyboard-layout -label:L10N -label:label-provider -label:languages-basic -label:languages-diagnostics -label:languages-guessing -label:layout -label:lcd-text-rendering -label:list -label:live-server -label:log -label:markdown -label:marketplace -label:menus -label:merge-conflict -label:network -label:notebook -label:notebook-api -label:notebook-celltoolbar -label:notebook-diff -label:notebook-dnd -label:notebook-folding -label:notebook-globaltoolbar -label:notebook-ipynb -label:notebook-kernel -label:notebook-keybinding -label:notebook-layout -label:notebook-markdown -label:notebook-minimap -label:notebook-multiselect -label:notebook-output -label:notebook-perf -label:notebook-statusbar -label:open-editors -label:opener -label:outline -label:output -label:perf -label:perf-bloat -label:perf-startup -label:php -label:portable-mode -label:proxy -label:quick-open -label:quick-pick -label:references-viewlet -label:release-notes -label:remote -label:remote-connection -label:remote-explorer -label:remotehub -label:rename -label:sandbox -label:sash -label:scm -label:screencast-mode -label:search -label:search-api -label:search-editor -label:search-replace -label:semantic-tokens -label:settings-editor -label:settings-sync -label:settings-sync-server -label:shared-process -label:simple-file-dialog -label:smart-select -label:snap -label:snippets -label:splitview -label:suggest -label:sync-error-handling -label:table -label:tasks -label:telemetry -label:terminal -label:terminal-conpty -label:terminal-editors -label:terminal-external -label:terminal-links -label:terminal-local-echo -label:terminal-profiles -label:terminal-reconnection -label:terminal-rendering -label:terminal-tabs -label:terminal-winpty -label:testing -label:themes -label:timeline -label:timeline-git -label:titlebar -label:tokenization -label:touch/pointer -label:trackpad/scroll -label:tree-views -label:tree-widget -label:typehierarchy -label:typescript -label:undo-redo -label:uri -label:ux -label:variable-resolving -label:VIM -label:virtual-workspaces -label:vscode-build -label:vscode-website -label:web -label:webview -label:webview-views -label:workbench-actions -label:workbench-cli -label:workbench-diagnostics -label:workbench-dnd -label:workbench-editor-grid -label:workbench-editor-groups -label:workbench-editor-resolver -label:workbench-editors -label:workbench-electron -label:workbench-feedback -label:workbench-history -label:workbench-hot-exit -label:workbench-hover -label:workbench-launch -label:workbench-link -label:workbench-multiroot -label:workbench-notifications -label:workbench-os-integration -label:workbench-rapid-render -label:workbench-run-as-admin -label:workbench-state -label:workbench-status -label:workbench-tabs -label:workbench-touchbar -label:workbench-untitled-editors -label:workbench-views -label:workbench-welcome -label:workbench-window -label:workbench-zen -label:workspace-edit -label:workspace-symbols -label:workspace-trust -label:zoom" }, { "kind": 1, @@ -113,5 +113,30 @@ "kind": 2, "language": "github-issues", "value": "$repos assignee:@me is:open label:\"needs more info\"" + }, + { + "kind": 1, + "language": "markdown", + "value": "### Pull Requests" + }, + { + "kind": 1, + "language": "markdown", + "value": "✅ Approved" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$repos author:@me is:open is:pr review:approved" + }, + { + "kind": 1, + "language": "markdown", + "value": "⌛ Pending Approval" + }, + { + "kind": 2, + "language": "github-issues", + "value": "$repos author:@me is:open is:pr review:required" } ] \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a6af100bec..f8ff902e2d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -95,7 +95,7 @@ "git", "sash" ], - "explorer.experimental.fileNesting.patterns": { + "explorer.fileNesting.patterns": { "*.js": "$(capture).*.js", "bootstrap.js": "bootstrap-*.js" } diff --git a/.yarnrc b/.yarnrc index 481bf7bfa46..44ce4c7d9a2 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,4 +1,4 @@ disturl "https://electronjs.org/headers" -target "13.5.2" +target "17.1.2" runtime "electron" build_from_source "true" diff --git a/README.md b/README.md index f6e8f39e2d5..022614a774b 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ VS Code includes a set of built-in extensions located in the [extensions](extens This repository includes a Visual Studio Code Remote - Containers / GitHub Codespaces development container. - For [Remote - Containers](https://aka.ms/vscode-remote/download/containers), use the **Remote-Containers: Clone Repository in Container Volume...** command which creates a Docker volume for better disk I/O on macOS and Windows. -- For Codespaces, install the [Github Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) extension in VS Code, and use the **Codespaces: Create New Codespace** command. +- For Codespaces, install the [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) extension in VS Code, and use the **Codespaces: Create New Codespace** command. Docker / the Codespace should have at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run full build. See the [development container README](.devcontainer/README.md) for more information. diff --git a/build/.cachesalt b/build/.cachesalt index b33738fb265..5eb7ff93bd8 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2022-02-28T10:04:13.972Z +2022-03-02T05:48:19.264Z diff --git a/build/azure-pipelines/common/createAsset.js b/build/azure-pipelines/common/createAsset.js index 43cdc96c063..389a052f469 100644 --- a/build/azure-pipelines/common/createAsset.js +++ b/build/azure-pipelines/common/createAsset.js @@ -85,12 +85,15 @@ function getPlatform(product, os, arch, type) { } return `darwin-${arch}`; case 'server': - return 'server-darwin'; - case 'web': - if (arch !== 'x64') { - throw new Error(`What should the platform be?: ${product} ${os} ${arch} ${type}`); + if (arch === 'x64') { + return 'server-darwin'; } - return 'server-darwin-web'; + return `server-darwin-${arch}`; + case 'web': + if (arch === 'x64') { + return 'server-darwin-web'; + } + return `server-darwin-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -132,7 +135,7 @@ async function main() { const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT'] || getEnv('BUILD_SOURCEVERSION'); console.log('Creating asset...'); const stat = await new Promise((c, e) => fs.stat(filePath, (err, stat) => err ? e(err) : c(stat))); const size = stat.size; diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 26c9e1a0b0e..07f833c39f6 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -100,12 +100,15 @@ function getPlatform(product: string, os: string, arch: string, type: string): s } return `darwin-${arch}`; case 'server': - return 'server-darwin'; - case 'web': - if (arch !== 'x64') { - throw new Error(`What should the platform be?: ${product} ${os} ${arch} ${type}`); + if (arch === 'x64') { + return 'server-darwin'; } - return 'server-darwin-web'; + return `server-darwin-${arch}`; + case 'web': + if (arch === 'x64') { + return 'server-darwin-web'; + } + return `server-darwin-${arch}-web`; default: throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); } @@ -154,7 +157,7 @@ async function main(): Promise { const platform = getPlatform(product, os, arch, unprocessedType); const type = getRealType(unprocessedType); const quality = getEnv('VSCODE_QUALITY'); - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT'] || getEnv('BUILD_SOURCEVERSION'); console.log('Creating asset...'); diff --git a/build/azure-pipelines/common/createBuild.js b/build/azure-pipelines/common/createBuild.js index cae7a456142..57bb87fedcf 100644 --- a/build/azure-pipelines/common/createBuild.js +++ b/build/azure-pipelines/common/createBuild.js @@ -21,9 +21,9 @@ function getEnv(name) { async function main() { const [, , _version] = process.argv; const quality = getEnv('VSCODE_QUALITY'); - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT']?.trim() || getEnv('BUILD_SOURCEVERSION'); const queuedBy = getEnv('BUILD_QUEUEDBY'); - const sourceBranch = getEnv('BUILD_SOURCEBRANCH'); + const sourceBranch = process.env['VSCODE_DISTRO_REF']?.trim() || getEnv('BUILD_SOURCEBRANCH'); const version = _version + (quality === 'stable' ? '' : `-${quality}`); console.log('Creating build...'); console.log('Quality:', quality); @@ -34,6 +34,7 @@ async function main() { timestamp: (new Date()).getTime(), version, isReleased: false, + private: Boolean(process.env['VSCODE_DISTRO_REF']?.trim()), sourceBranch, queuedBy, assets: [], @@ -42,7 +43,7 @@ async function main() { const aadCredentials = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], aadCredentials }); const scripts = client.database('builds').container(quality).scripts; - await (0, retry_1.retry)(() => scripts.storedProcedure('createBuild').execute('', [Object.assign(Object.assign({}, build), { _partitionKey: '' })])); + await (0, retry_1.retry)(() => scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }])); } main().then(() => { console.log('Build successfully created'); diff --git a/build/azure-pipelines/common/createBuild.ts b/build/azure-pipelines/common/createBuild.ts index 2877cc86303..8b1db94c412 100644 --- a/build/azure-pipelines/common/createBuild.ts +++ b/build/azure-pipelines/common/createBuild.ts @@ -27,9 +27,9 @@ function getEnv(name: string): string { async function main(): Promise { const [, , _version] = process.argv; const quality = getEnv('VSCODE_QUALITY'); - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT']?.trim() || getEnv('BUILD_SOURCEVERSION'); const queuedBy = getEnv('BUILD_QUEUEDBY'); - const sourceBranch = getEnv('BUILD_SOURCEBRANCH'); + const sourceBranch = process.env['VSCODE_DISTRO_REF']?.trim() || getEnv('BUILD_SOURCEBRANCH'); const version = _version + (quality === 'stable' ? '' : `-${quality}`); console.log('Creating build...'); @@ -42,6 +42,7 @@ async function main(): Promise { timestamp: (new Date()).getTime(), version, isReleased: false, + private: Boolean(process.env['VSCODE_DISTRO_REF']?.trim()), sourceBranch, queuedBy, assets: [], diff --git a/build/azure-pipelines/common/releaseBuild.js b/build/azure-pipelines/common/releaseBuild.js index 850d4ff2f15..26dacf4d731 100644 --- a/build/azure-pipelines/common/releaseBuild.js +++ b/build/azure-pipelines/common/releaseBuild.js @@ -29,7 +29,7 @@ async function getConfig(client, quality) { return res.resources[0]; } async function main() { - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT'] || getEnv('BUILD_SOURCEVERSION'); const quality = getEnv('VSCODE_QUALITY'); const aadCredentials = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], aadCredentials }); diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index e798ce490ba..064ac639e60 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -44,7 +44,7 @@ async function getConfig(client: CosmosClient, quality: string): Promise } async function main(): Promise { - const commit = getEnv('BUILD_SOURCEVERSION'); + const commit = process.env['VSCODE_DISTRO_COMMIT'] || getEnv('BUILD_SOURCEVERSION'); const quality = getEnv('VSCODE_QUALITY'); const aadCredentials = new ClientSecretCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_CLIENT_SECRET']!); diff --git a/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-sign.yml index 7d28236e2bf..8f6f0f42383 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -22,6 +22,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -38,7 +46,7 @@ steps: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --cwd build --frozen-lockfile && break + yarn --cwd build --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 375da9de1b7..6f4e8661cb2 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -37,6 +37,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -81,7 +89,7 @@ steps: export npm_config_node_gyp=$(which node-gyp) for i in {1..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 @@ -124,11 +132,11 @@ steps: - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-reh-darwin-min-ci + yarn gulp vscode-reh-darwin-$(VSCODE_ARCH)-min-ci VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn gulp vscode-reh-web-darwin-min-ci + yarn gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci displayName: Build Server - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - script: | set -e @@ -200,7 +208,7 @@ steps: APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ ./scripts/test-integration.sh --build --tfs "Integration Tests" displayName: Run integration tests (Electron) timeoutInMinutes: 20 @@ -208,7 +216,7 @@ steps: - script: | set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ ./scripts/test-web-integration.sh --browser webkit displayName: Run integration tests (Browser, Webkit) timeoutInMinutes: 20 @@ -219,7 +227,7 @@ steps: APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ ./scripts/test-remote-integration.sh displayName: Run integration tests (Remote) timeoutInMinutes: 20 @@ -227,7 +235,7 @@ steps: - script: | set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ yarn smoketest-no-compile --web --headless timeoutInMinutes: 10 displayName: Run smoke tests (Browser, Chromium) @@ -247,7 +255,7 @@ steps: set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) APP_NAME="`ls $APP_ROOT | head -n 1`" - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ yarn smoketest-no-compile --build "$APP_ROOT/$APP_NAME" --remote timeoutInMinutes: 10 displayName: Run smoke tests (Remote) @@ -296,27 +304,27 @@ steps: set -e # package Remote Extension Host - pushd .. && mv vscode-reh-darwin vscode-server-darwin && zip -Xry vscode-server-darwin.zip vscode-server-darwin && popd + pushd .. && mv vscode-reh-darwin-$(VSCODE_ARCH) vscode-server-darwin-$(VSCODE_ARCH) && zip -Xry vscode-server-darwin-$(VSCODE_ARCH).zip vscode-server-darwin-$(VSCODE_ARCH) && popd # package Remote Extension Host (Web) - pushd .. && mv vscode-reh-web-darwin vscode-server-darwin-web && zip -Xry vscode-server-darwin-web.zip vscode-server-darwin-web && popd + pushd .. && mv vscode-reh-web-darwin-$(VSCODE_ARCH) vscode-server-darwin-$(VSCODE_ARCH)-web && zip -Xry vscode-server-darwin-$(VSCODE_ARCH)-web.zip vscode-server-darwin-$(VSCODE_ARCH)-web && popd displayName: Prepare to publish servers - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Publish client archive condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - publish: $(Agent.BuildDirectory)/vscode-server-darwin.zip + - publish: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH).zip artifact: vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish server archive - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - - publish: $(Agent.BuildDirectory)/vscode-server-darwin-web.zip + - publish: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web.zip artifact: vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), ne(variables['VSCODE_PUBLISH'], 'false')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - task: AzureCLI@2 inputs: @@ -356,11 +364,11 @@ steps: - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 displayName: Generate SBOM (server) inputs: - BuildDropPath: $(agent.builddirectory)/vscode-server-darwin + BuildDropPath: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) PackageName: Visual Studio Code Server - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - - publish: $(agent.builddirectory)/vscode-server-darwin/_manifest + - publish: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)/_manifest displayName: Publish SBOM (server) artifact: vscode_server_darwin_$(VSCODE_ARCH)_sbom - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 53b62b47a4e..307f226dc0a 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -11,7 +11,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index 5b6599d8dab..a80650bb92d 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -7,7 +7,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index e425e670cd3..74577b52a68 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -42,6 +42,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -76,7 +84,7 @@ steps: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index 8b4b20a50ea..35aad220604 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -50,6 +50,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -84,7 +92,7 @@ steps: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --cwd build --frozen-lockfile && break + yarn --cwd build --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 @@ -99,7 +107,7 @@ steps: if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/91.0.4472.164/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/98.0.4758.109/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 \ @@ -109,19 +117,19 @@ steps: 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/91.0.4472.164:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/91.0.4472.164:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/91.0.4472.164:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109: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_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -D__NO_INLINE__ -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -isystem$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" 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..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/linux/product-build-linux-server.yml b/build/azure-pipelines/linux/product-build-linux-server.yml index cbda3510d62..94145131c37 100644 --- a/build/azure-pipelines/linux/product-build-linux-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-server.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -22,6 +22,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -40,7 +48,7 @@ steps: export npm_config_arch=$(NPM_ARCH) for i in {1..3}; do # try 3 times, for Terrapin - yarn --cwd remote --frozen-lockfile && break + yarn --cwd remote --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 @@ -48,6 +56,8 @@ steps: echo "Yarn failed $i, trying again..." done displayName: Install dependencies + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) - script: | diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index 33a80b74391..12829334956 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: DownloadPipelineArtifact@0 displayName: "Download Pipeline Artifact" diff --git a/build/azure-pipelines/mixin.js b/build/azure-pipelines/mixin.js index ba73cdeb083..769ac72a9ee 100644 --- a/build/azure-pipelines/mixin.js +++ b/build/azure-pipelines/mixin.js @@ -43,7 +43,7 @@ async function mixinClient(quality) { else { fancyLog(ansiColors.blue('[mixin]'), 'Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); } - return Object.assign(Object.assign({ webBuiltInExtensions: originalProduct.webBuiltInExtensions }, o), { builtInExtensions }); + return { webBuiltInExtensions: originalProduct.webBuiltInExtensions, ...o, builtInExtensions }; })) .pipe(productJsonFilter.restore) .pipe(es.mapSync((f) => { @@ -64,7 +64,7 @@ function mixinServer(quality) { fancyLog(ansiColors.blue('[mixin]'), `Mixing in server:`); const originalProduct = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'product.json'), 'utf8')); const serverProductJson = JSON.parse(fs.readFileSync(serverProductJsonPath, 'utf8')); - fs.writeFileSync('product.json', JSON.stringify(Object.assign(Object.assign({}, originalProduct), serverProductJson), undefined, '\t')); + fs.writeFileSync('product.json', JSON.stringify({ ...originalProduct, ...serverProductJson }, undefined, '\t')); fancyLog(ansiColors.blue('[mixin]'), 'product.json', ansiColors.green('✔︎')); } function main() { diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index 417b7888a84..551228f420b 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -9,6 +9,10 @@ schedules: - joao/web parameters: + - name: VSCODE_DISTRO_REF + displayName: Distro Ref (Private build) + type: string + default: " " - name: VSCODE_QUALITY displayName: Quality type: string @@ -87,12 +91,12 @@ parameters: default: false variables: + - name: VSCODE_DISTRO_REF + value: ${{ parameters.VSCODE_DISTRO_REF }} - name: ENABLE_TERRAPIN value: ${{ eq(parameters.ENABLE_TERRAPIN, true) }} - name: VSCODE_QUALITY value: ${{ parameters.VSCODE_QUALITY }} - - name: VSCODE_RELEASE - value: ${{ parameters.VSCODE_RELEASE }} - name: VSCODE_BUILD_STAGE_WINDOWS value: ${{ or(eq(parameters.VSCODE_BUILD_WIN32, true), eq(parameters.VSCODE_BUILD_WIN32_32BIT, true), eq(parameters.VSCODE_BUILD_WIN32_ARM64, true)) }} - name: VSCODE_BUILD_STAGE_LINUX @@ -362,7 +366,7 @@ stages: steps: - template: product-publish.yml - - ${{ if or(eq(parameters.VSCODE_RELEASE, true), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: + - ${{ if or(and(parameters.VSCODE_RELEASE, eq(parameters.VSCODE_DISTRO_REF, ' ')), and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(variables['VSCODE_SCHEDULEDBUILD'], true))) }}: - stage: Release dependsOn: - Publish diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 44364c76bec..7722c3e069c 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -22,6 +22,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -64,7 +72,7 @@ steps: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index c43180ea0a3..4d711aba120 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -10,11 +10,36 @@ steps: KeyVaultName: vscode SecretsFilter: "github-distro-mixin-password" + - script: | + set -e + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + displayName: Prepare tooling + + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + + - script: | + set -e + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") + displayName: Merge distro + - pwsh: | . build/azure-pipelines/win32/exec.ps1 cd build exec { yarn } - displayName: Install dependencies + displayName: Install build dependencies - download: current patterns: "**/artifacts_processed_*.txt" diff --git a/build/azure-pipelines/product-release.yml b/build/azure-pipelines/product-release.yml index fa6396b1486..a1086945595 100644 --- a/build/azure-pipelines/product-release.yml +++ b/build/azure-pipelines/product-release.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureCLI@2 inputs: diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 043bb5141ba..031fafd4c75 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -12,7 +12,7 @@ pool: steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - bash: | TAG_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) diff --git a/build/azure-pipelines/sdl-scan.yml b/build/azure-pipelines/sdl-scan.yml index 0b4e16d4570..e175779f850 100644 --- a/build/azure-pipelines/sdl-scan.yml +++ b/build/azure-pipelines/sdl-scan.yml @@ -47,7 +47,7 @@ stages: outputFormat: "pre" - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -65,6 +65,15 @@ stages: exec { git config user.name "VSCode" } displayName: Prepare tooling + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + + exec { git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $(VSCODE_DISTRO_REF) } + exec { git checkout FETCH_HEAD } + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -84,7 +93,7 @@ stages: inputs: sourceCodeDirectory: "$(Build.SourcesDirectory)" language: "cpp" - buildCommandsString: "yarn --frozen-lockfile" + buildCommandsString: "yarn --frozen-lockfile --check-files" querySuite: "Required" timeout: "1800" ram: "16384" @@ -99,7 +108,7 @@ stages: . build/azure-pipelines/win32/exec.ps1 . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" - retry { exec { yarn --frozen-lockfile } } + retry { exec { yarn --frozen-lockfile --check-files } } env: npm_config_arch: "$(NPM_ARCH)" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 @@ -139,7 +148,7 @@ stages: toolMajorVersion: "V2" - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -160,6 +169,14 @@ stages: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -176,7 +193,7 @@ stages: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --cwd build --frozen-lockfile && break + yarn --cwd build --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 @@ -191,7 +208,7 @@ stages: if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/91.0.4472.164/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/96.0.4664.110/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 \ @@ -202,14 +219,14 @@ stages: # Set compiler toolchain export CC=$PWD/.build/CR_Clang/bin/clang export CXX=$PWD/.build/CR_Clang/bin/clang++ - export CXXFLAGS="-nostdinc++ -D_LIBCPP_HAS_NO_VENDOR_AVAILABILITY_ANNOTATIONS -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" + export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -isystem$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit" export LDFLAGS="-stdlib=libc++ -fuse-ld=lld -flto=thin -fsplit-lto-unit -L$PWD/.build/libcxx-objects -lc++abi" export VSCODE_REMOTE_CC=$(which gcc) export VSCODE_REMOTE_CXX=$(which g++) fi for i in {1..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/upload-cdn.js b/build/azure-pipelines/upload-cdn.js index 9d2c3a47ae1..1bd3ed6f5fd 100644 --- a/build/azure-pipelines/upload-cdn.js +++ b/build/azure-pipelines/upload-cdn.js @@ -4,17 +4,14 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); const es = require("event-stream"); const Vinyl = require("vinyl"); const vfs = require("vinyl-fs"); -const util = require("../lib/util"); const filter = require("gulp-filter"); const gzip = require("gulp-gzip"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); async function main() { const files = []; diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index c6180507991..9ac656e7c26 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -5,18 +5,15 @@ 'use strict'; -import * as path from 'path'; import * as es from 'event-stream'; import * as Vinyl from 'vinyl'; import * as vfs from 'vinyl-fs'; -import * as util from '../lib/util'; import * as filter from 'gulp-filter'; import * as gzip from 'gulp-gzip'; import { ClientSecretCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new ClientSecretCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_CLIENT_SECRET']!); async function main(): Promise { diff --git a/build/azure-pipelines/upload-configuration.js b/build/azure-pipelines/upload-configuration.js index 689d99fdae0..40440108773 100644 --- a/build/azure-pipelines/upload-configuration.js +++ b/build/azure-pipelines/upload-configuration.js @@ -13,8 +13,7 @@ const util = require("../lib/util"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const packageJson = require("../../package.json"); -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; function generateVSCodeConfigurationTask() { return new Promise((resolve, reject) => { const buildDir = process.env['AGENT_BUILDDIRECTORY']; diff --git a/build/azure-pipelines/upload-configuration.ts b/build/azure-pipelines/upload-configuration.ts index 3acc337e749..f39175ae91d 100644 --- a/build/azure-pipelines/upload-configuration.ts +++ b/build/azure-pipelines/upload-configuration.ts @@ -14,8 +14,7 @@ import { ClientSecretCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); import * as packageJson from '../../package.json'; -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; function generateVSCodeConfigurationTask(): Promise { return new Promise((resolve, reject) => { diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js index af7ad62e9ff..a17b8e71267 100644 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ b/build/azure-pipelines/upload-nlsmetadata.js @@ -4,16 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); const es = require("event-stream"); const vfs = require("vinyl-fs"); -const util = require("../lib/util"); const merge = require("gulp-merge-json"); const gzip = require("gulp-gzip"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); function main() { return new Promise((c, e) => { diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 82f2349f9f9..f1d5b5f4d8b 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -5,18 +5,15 @@ 'use strict'; -import * as path from 'path'; import * as es from 'event-stream'; import * as Vinyl from 'vinyl'; import * as vfs from 'vinyl-fs'; -import * as util from '../lib/util'; import * as merge from 'gulp-merge-json'; import * as gzip from 'gulp-gzip'; import { ClientSecretCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); -const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new ClientSecretCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_CLIENT_SECRET']!); interface NlsMetadata { diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js index 4edcd2ccd70..22e3e28290d 100644 --- a/build/azure-pipelines/upload-sourcemaps.js +++ b/build/azure-pipelines/upload-sourcemaps.js @@ -13,7 +13,7 @@ const deps = require("../lib/dependencies"); const identity_1 = require("@azure/identity"); const azure = require('gulp-azure-storage'); const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new identity_1.ClientSecretCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_CLIENT_SECRET']); // optionally allow to pass in explicit base/maps to upload const [, , base, maps] = process.argv; diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index ed092c0df1c..e78e783c064 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -16,7 +16,7 @@ import { ClientSecretCredential } from '@azure/identity'; const azure = require('gulp-azure-storage'); const root = path.dirname(path.dirname(__dirname)); -const commit = util.getVersion(root); +const commit = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; const credential = new ClientSecretCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, process.env['AZURE_CLIENT_SECRET']!); // optionally allow to pass in explicit base/maps to upload diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index aced8b3076f..fa07a82305d 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -33,6 +33,14 @@ steps: git config user.name "VSCode" displayName: Prepare tooling + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - script: | set -e git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") @@ -67,7 +75,7 @@ steps: - script: | set -e for i in {1..3}; do # try 3 times, for Terrapin - yarn --frozen-lockfile && break + yarn --frozen-lockfile --check-files && break if [ $i -eq 3 ]; then echo "Yarn failed too many times" >&2 exit 1 diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 49d2e4d7d56..001ce407218 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "14.x" + versionSpec: "16.x" - task: UsePythonVersion@0 inputs: @@ -36,6 +36,16 @@ steps: exec { git config user.name "VSCode" } displayName: Prepare tooling + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + + exec { git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $(VSCODE_DISTRO_REF) } + Write-Host "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + exec { git checkout FETCH_HEAD } + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -77,7 +87,7 @@ steps: $ErrorActionPreference = "Stop" $env:npm_config_arch="$(VSCODE_ARCH)" $env:CHILD_CONCURRENCY="1" - retry { exec { yarn --frozen-lockfile } } + retry { exec { yarn --frozen-lockfile --check-files } } env: ELECTRON_SKIP_BINARY_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 diff --git a/build/builtin/browser-main.js b/build/builtin/browser-main.js index d105181d634..cf95c12cd95 100644 --- a/build/builtin/browser-main.js +++ b/build/builtin/browser-main.js @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +//@ts-check const fs = require('fs'); const path = require('path'); @@ -11,14 +12,28 @@ const { ipcRenderer } = require('electron'); const builtInExtensionsPath = path.join(__dirname, '..', '..', 'product.json'); const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); +/** + * @param {string} filePath + */ function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, { encoding: 'utf8' })); } +/** + * @param {string} filePath + * @param {any} obj + */ function writeJson(filePath, obj) { fs.writeFileSync(filePath, JSON.stringify(obj, null, 2)); } +/** + * @param {HTMLFormElement} form + * @param {string} id + * @param {string} title + * @param {string} value + * @param {boolean} checked + */ function renderOption(form, id, title, value, checked) { const input = document.createElement('input'); input.type = 'radio'; @@ -36,7 +51,14 @@ function renderOption(form, id, title, value, checked) { return input; } +/** + * @param {HTMLElement} el + * @param {any} state + */ function render(el, state) { + /** + * @param {any} state + */ function setState(state) { try { writeJson(controlFilePath, state.control); @@ -114,7 +136,9 @@ function main() { control = {}; } - render(el, { builtin, control }); + if (el) { + render(el, { builtin, control }); + } } window.onload = main; diff --git a/build/builtin/main.js b/build/builtin/main.js index 34df1fab6df..b1842e9a595 100644 --- a/build/builtin/main.js +++ b/build/builtin/main.js @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// @ts-check const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const url = require('url'); diff --git a/build/darwin/sign.js b/build/darwin/sign.js index 08993aebdeb..02b0e1f052e 100644 --- a/build/darwin/sign.js +++ b/build/darwin/sign.js @@ -5,11 +5,10 @@ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); const codesign = require("electron-osx-sign"); -const fs = require("fs-extra"); const path = require("path"); -const plist = require("plist"); const util = require("../lib/util"); const product = require("../../product.json"); +const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); async function main() { const buildDir = process.env['AGENT_BUILDDIRECTORY']; const tempDir = process.env['AGENT_TEMPDIRECTORY']; @@ -41,22 +40,51 @@ async function main() { identity: '99FM488X57', 'gatekeeper-assess': false }; - const appOpts = Object.assign(Object.assign({}, defaultOpts), { + const appOpts = { + ...defaultOpts, // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath) => { return filePath.includes(gpuHelperAppName) || filePath.includes(rendererHelperAppName); - } }); - const gpuHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, gpuHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist') }); - const rendererHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, rendererHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist') }); - let infoPlistString = await fs.readFile(infoPlistPath, 'utf8'); - let infoPlistJson = plist.parse(infoPlistString); - Object.assign(infoPlistJson, { - NSAppleEventsUsageDescription: 'An application in Visual Studio Code wants to use AppleScript.', - NSMicrophoneUsageDescription: 'An application in Visual Studio Code wants to use the Microphone.', - NSCameraUsageDescription: 'An application in Visual Studio Code wants to use the Camera.' - }); - await fs.writeFile(infoPlistPath, plist.build(infoPlistJson), 'utf8'); + } + }; + const gpuHelperOpts = { + ...defaultOpts, + app: path.join(appFrameworkPath, gpuHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + }; + const rendererHelperOpts = { + ...defaultOpts, + app: path.join(appFrameworkPath, rendererHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + }; + // Only overwrite plist entries for x64 and arm64 builds, + // universal will get its copy from the x64 build. + if (arch !== 'universal') { + await (0, cross_spawn_promise_1.spawn)('plutil', [ + '-insert', + 'NSAppleEventsUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use AppleScript.', + `${infoPlistPath}` + ]); + await (0, cross_spawn_promise_1.spawn)('plutil', [ + '-replace', + 'NSMicrophoneUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use the Microphone.', + `${infoPlistPath}` + ]); + await (0, cross_spawn_promise_1.spawn)('plutil', [ + '-replace', + 'NSCameraUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use the Camera.', + `${infoPlistPath}` + ]); + } await codesign.signAsync(gpuHelperOpts); await codesign.signAsync(rendererHelperOpts); await codesign.signAsync(appOpts); diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 8f4e2cb2ad8..9b0a9f75afc 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -6,11 +6,10 @@ 'use strict'; import * as codesign from 'electron-osx-sign'; -import * as fs from 'fs-extra'; import * as path from 'path'; -import * as plist from 'plist'; import * as util from '../lib/util'; import * as product from '../../product.json'; +import { spawn } from '@malept/cross-spawn-promise'; async function main(): Promise { const buildDir = process.env['AGENT_BUILDDIRECTORY']; @@ -71,14 +70,31 @@ async function main(): Promise { 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), }; - let infoPlistString = await fs.readFile(infoPlistPath, 'utf8'); - let infoPlistJson = plist.parse(infoPlistString); - Object.assign(infoPlistJson, { - NSAppleEventsUsageDescription: 'An application in Visual Studio Code wants to use AppleScript.', - NSMicrophoneUsageDescription: 'An application in Visual Studio Code wants to use the Microphone.', - NSCameraUsageDescription: 'An application in Visual Studio Code wants to use the Camera.' - }); - await fs.writeFile(infoPlistPath, plist.build(infoPlistJson), 'utf8'); + // Only overwrite plist entries for x64 and arm64 builds, + // universal will get its copy from the x64 build. + if (arch !== 'universal') { + await spawn('plutil', [ + '-insert', + 'NSAppleEventsUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use AppleScript.', + `${infoPlistPath}` + ]); + await spawn('plutil', [ + '-replace', + 'NSMicrophoneUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use the Microphone.', + `${infoPlistPath}` + ]); + await spawn('plutil', [ + '-replace', + 'NSCameraUsageDescription', + '-string', + 'An application in Visual Studio Code wants to use the Camera.', + `${infoPlistPath}` + ]); + } await codesign.signAsync(gpuHelperOpts); await codesign.signAsync(rendererHelperOpts); diff --git a/build/filters.js b/build/filters.js index 87ded512d05..f462566d0f4 100644 --- a/build/filters.js +++ b/build/filters.js @@ -121,9 +121,12 @@ module.exports.indentationFilter = [ '!**/Dockerfile.*', '!**/*.Dockerfile', '!**/*.dockerfile', + + // except for built files '!extensions/markdown-language-features/media/*.js', '!extensions/markdown-language-features/notebook-out/*.js', '!extensions/markdown-math/notebook-out/*.js', + '!extensions/notebook-renderers/renderer-out/*.js', '!extensions/simple-browser/media/*.js', ]; diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index c8308a0d190..41dbfe647ae 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -235,6 +235,9 @@ const appendJSToESMImportsTask = task.define('append-js-to-esm-imports', () => { } }); +/** + * @param {string} contents + */ function toExternalDTS(contents) { let lines = contents.split(/\r\n|\r|\n/); let killNextCloseCurlyBrace = false; @@ -278,6 +281,9 @@ function toExternalDTS(contents) { return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); } +/** + * @param {{ (path: string): boolean }} testFunc + */ function filterStream(testFunc) { return es.through(function (data) { if (!testFunc(data.relative)) { @@ -479,6 +485,8 @@ function createTscCompileTask(watch) { }); let errors = []; let reporter = createReporter('monaco'); + + /** @type {NodeJS.ReadWriteStream | undefined} */ let report; // eslint-disable-next-line no-control-regex let magic = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; // https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index 89517f86fef..cdc02cba74b 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -239,6 +239,9 @@ const watchWebExtensionsTask = task.define('watch-web', () => buildWebExtensions gulp.task(watchWebExtensionsTask); exports.watchWebExtensionsTask = watchWebExtensionsTask; +/** + * @param {boolean} isWatch + */ async function buildWebExtensions(isWatch) { const webpackConfigLocations = await nodeUtil.promisify(glob)( path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 838fc661eb5..a9691fcb0d8 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -9,6 +9,9 @@ const path = require('path'); const task = require('./lib/task'); const { hygiene } = require('./hygiene'); +/** + * @param {string} actualPath + */ function checkPackageJSON(actualPath) { const actual = require(path.join(__dirname, '..', actualPath)); const rootPackageJSON = require('../package.json'); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index e99a1a1a306..757f670cff2 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -39,7 +39,8 @@ const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); const BUILD_TARGETS = [ { platform: 'win32', arch: 'ia32' }, { platform: 'win32', arch: 'x64' }, - { platform: 'darwin', arch: null }, + { platform: 'darwin', arch: 'x64' }, + { platform: 'darwin', arch: 'arm64' }, { platform: 'linux', arch: 'ia32' }, { platform: 'linux', arch: 'x64' }, { platform: 'linux', arch: 'armhf' }, @@ -132,10 +133,6 @@ function getNodeVersion() { const nodeVersion = getNodeVersion(); BUILD_TARGETS.forEach(({ platform, arch }) => { - if (platform === 'darwin') { - arch = 'x64'; - } - gulp.task(task.define(`node-${platform}-${arch}`, () => { const nodePath = path.join('.build', 'node', `v${nodeVersion}`, `${platform}-${arch}`); @@ -150,8 +147,7 @@ BUILD_TARGETS.forEach(({ platform, arch }) => { })); }); -const arch = process.platform === 'darwin' ? 'x64' : process.arch; -const defaultNodeTask = gulp.task(`node-${process.platform}-${arch}`); +const defaultNodeTask = gulp.task(`node-${process.platform}-${process.arch}`); if (defaultNodeTask) { gulp.task(task.define('node', defaultNodeTask)); @@ -176,10 +172,6 @@ function nodejs(platform, arch) { return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]); } - if (platform === 'darwin') { - arch = 'x64'; - } - if (arch === 'armhf') { arch = 'armv7l'; } @@ -273,7 +265,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa .pipe(util.stripSourceMappingURL()) .pipe(jsFilter.restore); - const nodePath = `.build/node/v${nodeVersion}/${platform}-${platform === 'darwin' ? 'x64' : arch}`; + const nodePath = `.build/node/v${nodeVersion}/${platform}-${arch}`; const node = gulp.src(`${nodePath}/**`, { base: nodePath, dot: true }); let web = []; @@ -390,7 +382,7 @@ function tweakProductForServerWeb(product) { const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( - gulp.task(`node-${platform}-${platform === 'darwin' ? 'x64' : arch}`), + gulp.task(`node-${platform}-${arch}`), util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), packageTask(type, platform, arch, sourceFolderName, destinationFolderName) )); diff --git a/build/gulpfile.scan.js b/build/gulpfile.scan.js index cf88b69e0f6..f9b0f689977 100644 --- a/build/gulpfile.scan.js +++ b/build/gulpfile.scan.js @@ -29,7 +29,7 @@ const BUILD_TARGETS = [ ]; BUILD_TARGETS.forEach(buildTarget => { - const dashed = (str) => (str ? `-${str}` : ``); + const dashed = (/** @type {string | null} */ str) => (str ? `-${str}` : ``); const platform = buildTarget.platform; const arch = buildTarget.arch; @@ -76,7 +76,14 @@ function nodeModules(destinationExe, destinationPdb, platform) { const exe = () => { return gulp.src(dependenciesSrc, { base: '.', dot: true }) - .pipe(filter(['**/*.node', '!**/prebuilds/**/*.node'])) + .pipe(filter([ + '**/*.node', + // Exclude these paths. + // We don't build the prebuilt node files so we don't scan them + '!**/prebuilds/**/*.node', + // These are 3rd party modules that we should ignore + '!**/@parcel/watcher/**/*', + '!**/native-is-elevated/**/*'])) .pipe(gulp.dest(destinationExe)); }; diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 3423e86e8f3..3c6a7b3ee89 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -15,7 +15,7 @@ const util = require('./lib/util'); const task = require('./lib/task'); const packageJson = require('../package.json'); const product = require('../product.json'); -const rpmDependencies = require('../resources/linux/rpm/dependencies.json'); +const rpmDependenciesGenerator = require('./linux/rpm/dependencies-generator'); const path = require('path'); const root = path.dirname(__dirname); const commit = util.getVersion(root); @@ -103,6 +103,9 @@ function prepareDebPackage(arch) { }; } +/** + * @param {string} arch + */ function buildDebPackage(arch) { const debArch = getDebPackageArch(arch); return shell.task([ @@ -112,14 +115,23 @@ function buildDebPackage(arch) { ], { cwd: '.build/linux/deb/' + debArch }); } +/** + * @param {string} rpmArch + */ function getRpmBuildPath(rpmArch) { return '.build/linux/rpm/' + rpmArch + '/rpmbuild'; } +/** + * @param {string} arch + */ function getRpmPackageArch(arch) { return { x64: 'x86_64', armhf: 'armv7hl', arm64: 'aarch64' }[arch]; } +/** + * @param {string} arch + */ function prepareRpmPackage(arch) { const binaryDir = '../VSCode-linux-' + arch; const rpmArch = getRpmPackageArch(arch); @@ -164,6 +176,7 @@ function prepareRpmPackage(arch) { const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) .pipe(rename(function (p) { p.dirname = 'BUILD/usr/share/' + product.applicationName + '/' + p.dirname; })); + const dependencies = rpmDependenciesGenerator.getDependencies(binaryDir, product.applicationName); const spec = gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@NAME_LONG@@', product.nameLong)) @@ -174,7 +187,7 @@ function prepareRpmPackage(arch) { .pipe(replace('@@LICENSE@@', product.licenseName)) .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) - .pipe(replace('@@DEPENDENCIES@@', rpmDependencies[rpmArch].join(', '))) + .pipe(replace('@@DEPENDENCIES@@', dependencies.join(', '))) .pipe(rename('SPECS/' + product.applicationName + '.spec')); const specIcon = gulp.src('resources/linux/rpm/code.xpm', { base: '.' }) @@ -186,6 +199,9 @@ function prepareRpmPackage(arch) { }; } +/** + * @param {string} arch + */ function buildRpmPackage(arch) { const rpmArch = getRpmPackageArch(arch); const rpmBuildPath = getRpmBuildPath(rpmArch); @@ -199,10 +215,16 @@ function buildRpmPackage(arch) { ]); } +/** + * @param {string} arch + */ function getSnapBuildPath(arch) { return `.build/linux/snap/${arch}/${product.applicationName}-${arch}`; } +/** + * @param {string} arch + */ function prepareSnapPackage(arch) { const binaryDir = '../VSCode-linux-' + arch; const destination = getSnapBuildPath(arch); @@ -247,6 +269,9 @@ function prepareSnapPackage(arch) { }; } +/** + * @param {string} arch + */ function buildSnapPackage(arch) { const snapBuildPath = getSnapBuildPath(arch); // Default target for snapcraft runs: pull, build, stage and prime, and finally assembles the snap. diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index e93fbf30008..8cc0e41d794 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -90,7 +90,6 @@ const createVSCodeWebProductConfigurationPatcher = (product) => { if (path.endsWith('vs/platform/product/common/product.js')) { const productConfiguration = JSON.stringify({ ...product, - extensionAllowedProposedApi: [...product.extensionAllowedProposedApi], version, commit, date: buildDate @@ -235,7 +234,7 @@ const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build' )); gulp.task(compileWebExtensionsBuildTask); -const dashed = (str) => (str ? `-${str}` : ``); +const dashed = (/** @type {string} */ str) => (str ? `-${str}` : ``); ['', 'min'].forEach(minified => { const sourceFolderName = `out-vscode-web${dashed(minified)}`; diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 1a73ae9d71b..81ba5095816 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -20,10 +20,10 @@ const rcedit = require('rcedit'); const mkdirp = require('mkdirp'); const repoPath = path.dirname(__dirname); -const buildPath = arch => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); -const zipDir = arch => path.join(repoPath, '.build', `win32-${arch}`, 'archive'); -const zipPath = arch => path.join(zipDir(arch), `VSCode-win32-${arch}.zip`); -const setupDir = (arch, target) => path.join(repoPath, '.build', `win32-${arch}`, `${target}-setup`); +const buildPath = (/** @type {string} */ arch) => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); +const zipDir = (/** @type {string} */ arch) => path.join(repoPath, '.build', `win32-${arch}`, 'archive'); +const zipPath = (/** @type {string} */ arch) => path.join(zipDir(arch), `VSCode-win32-${arch}.zip`); +const setupDir = (/** @type {string} */ arch, /** @type {string} */ target) => path.join(repoPath, '.build', `win32-${arch}`, `${target}-setup`); const issPath = path.join(__dirname, 'win32', 'code.iss'); const innoSetupPath = path.join(path.dirname(path.dirname(require.resolve('innosetup'))), 'bin', 'ISCC.exe'); const signWin32Path = path.join(repoPath, 'build', 'azure-pipelines', 'common', 'sign-win32'); @@ -63,6 +63,10 @@ function packageInnoSetup(iss, options, cb) { }); } +/** + * @param {string} arch + * @param {string} target + */ function buildWin32Setup(arch, target) { if (target !== 'system' && target !== 'user') { throw new Error('Invalid setup target'); @@ -112,6 +116,10 @@ function buildWin32Setup(arch, target) { }; } +/** + * @param {string} arch + * @param {string} target + */ function defineWin32SetupTasks(arch, target) { const cleanTask = util.rimraf(setupDir(arch, target)); gulp.task(task.define(`vscode-win32-${arch}-${target}-setup`, task.series(cleanTask, buildWin32Setup(arch, target)))); @@ -124,6 +132,9 @@ defineWin32SetupTasks('ia32', 'user'); defineWin32SetupTasks('x64', 'user'); defineWin32SetupTasks('arm64', 'user'); +/** + * @param {string} arch + */ function archiveWin32Setup(arch) { return cb => { const args = ['a', '-tzip', zipPath(arch), '-x!CodeSignSummary*.md', '.', '-r']; @@ -138,6 +149,9 @@ gulp.task(task.define('vscode-win32-ia32-archive', task.series(util.rimraf(zipDi gulp.task(task.define('vscode-win32-x64-archive', task.series(util.rimraf(zipDir('x64')), archiveWin32Setup('x64')))); gulp.task(task.define('vscode-win32-arm64-archive', task.series(util.rimraf(zipDir('arm64')), archiveWin32Setup('arm64')))); +/** + * @param {string} arch + */ function copyInnoUpdater(arch) { return () => { return gulp.src('build/win32/{inno_updater.exe,vcruntime140.dll}', { base: 'build/win32' }) @@ -145,6 +159,9 @@ function copyInnoUpdater(arch) { }; } +/** + * @param {string} executablePath + */ function updateIcon(executablePath) { return cb => { const icon = path.join(repoPath, 'resources', 'win32', 'code.ico'); diff --git a/build/jsconfig.json b/build/jsconfig.json deleted file mode 100644 index 299294ef055..00000000000 --- a/build/jsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es2017", - "jsx": "preserve", - "checkJs": true - }, - "include": [ - "**/*.js" - ], - "exclude": [ - "node_modules", - "**/node_modules/*" - ] -} \ No newline at end of file diff --git a/build/lib/builtInExtensionsCG.js b/build/lib/builtInExtensionsCG.js index 435cfcf8035..3292914de37 100644 --- a/build/lib/builtInExtensionsCG.js +++ b/build/lib/builtInExtensionsCG.js @@ -18,7 +18,6 @@ const token = process.env['VSCODE_MIXIN_PASSWORD'] || process.env['GITHUB_TOKEN' const contentBasePath = 'raw.githubusercontent.com'; const contentFileNames = ['package.json', 'package-lock.json', 'yarn.lock']; async function downloadExtensionDetails(extension) { - var _a, _b, _c; const extensionLabel = `${extension.name}@${extension.version}`; const repository = url.parse(extension.repo).path.substr(1); const repositoryContentBaseUrl = `https://${token ? `${token}@` : ''}${contentBasePath}/${repository}/v${extension.version}`; @@ -56,11 +55,11 @@ async function downloadExtensionDetails(extension) { } } // Validation - if (!((_a = results.find(r => r.fileName === 'package.json')) === null || _a === void 0 ? void 0 : _a.body)) { + if (!results.find(r => r.fileName === 'package.json')?.body) { // throw new Error(`The "package.json" file could not be found for the built-in extension - ${extensionLabel}`); } - if (!((_b = results.find(r => r.fileName === 'package-lock.json')) === null || _b === void 0 ? void 0 : _b.body) && - !((_c = results.find(r => r.fileName === 'yarn.lock')) === null || _c === void 0 ? void 0 : _c.body)) { + if (!results.find(r => r.fileName === 'package-lock.json')?.body && + !results.find(r => r.fileName === 'yarn.lock')?.body) { // throw new Error(`The "package-lock.json"/"yarn.lock" could not be found for the built-in extension - ${extensionLabel}`); } } diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 143e64e087d..8c1967d4c68 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -22,10 +22,10 @@ function bundle(entryPoints, config, callback) { const allMentionedModulesMap = {}; entryPoints.forEach((module) => { allMentionedModulesMap[module.name] = true; - (module.include || []).forEach(function (includedModule) { + module.include?.forEach(function (includedModule) { allMentionedModulesMap[includedModule] = true; }); - (module.exclude || []).forEach(function (excludedModule) { + module.exclude?.forEach(function (excludedModule) { allMentionedModulesMap[excludedModule] = true; }); }); diff --git a/build/lib/bundle.ts b/build/lib/bundle.ts index 892ced8b85b..a1130d4bbbd 100644 --- a/build/lib/bundle.ts +++ b/build/lib/bundle.ts @@ -109,10 +109,10 @@ export function bundle(entryPoints: IEntryPoint[], config: ILoaderConfig, callba const allMentionedModulesMap: { [modules: string]: boolean } = {}; entryPoints.forEach((module: IEntryPoint) => { allMentionedModulesMap[module.name] = true; - (module.include || []).forEach(function (includedModule) { + module.include?.forEach(function (includedModule) { allMentionedModulesMap[includedModule] = true; }); - (module.exclude || []).forEach(function (excludedModule) { + module.exclude?.forEach(function (excludedModule) { allMentionedModulesMap[excludedModule] = true; }); }); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 5c160b7b7aa..1841795cefd 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -38,7 +38,7 @@ function createCompile(src, build, emitError) { const tsb = require('gulp-tsb'); const sourcemaps = require('gulp-sourcemaps'); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); - const overrideOptions = Object.assign(Object.assign({}, getTypeScriptCompilerOptions(src)), { inlineSources: Boolean(build) }); + const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; } diff --git a/build/lib/dependencies.js b/build/lib/dependencies.js index b6a399267b4..cbc6cec9d81 100644 --- a/build/lib/dependencies.js +++ b/build/lib/dependencies.js @@ -36,7 +36,7 @@ function asYarnDependency(prefix, tree) { return { name, version, path: dependencyPath, children }; } function getYarnProductionDependencies(cwd) { - const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'production' }), stdio: [null, null, 'inherit'] }); + const raw = cp.execSync('yarn list --json', { cwd, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, 'inherit'] }); const match = /^{"type":"tree".*$/m.exec(raw); if (!match || match.length !== 1) { throw new Error('Could not parse result of `yarn list --json`'); diff --git a/build/lib/electron.js b/build/lib/electron.js index 3c4a3a78045..f623e8cb1b0 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -40,7 +40,7 @@ const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSyn function darwinBundleDocumentType(extensions, icon, nameOrSuffix) { // If given a suffix, generate a name from it. If not given anything, default to 'document' if (isDocumentSuffix(nameOrSuffix) || !nameOrSuffix) { - nameOrSuffix = icon.charAt(0).toUpperCase() + icon.slice(1) + ' ' + (nameOrSuffix !== null && nameOrSuffix !== void 0 ? nameOrSuffix : 'document'); + nameOrSuffix = icon.charAt(0).toUpperCase() + icon.slice(1) + ' ' + (nameOrSuffix ?? 'document'); } return { name: nameOrSuffix, diff --git a/build/lib/eslint/vscode-dts-create-func.js b/build/lib/eslint/vscode-dts-create-func.js index 5a27bf51c80..e9ec659cef1 100644 --- a/build/lib/eslint/vscode-dts-create-func.js +++ b/build/lib/eslint/vscode-dts-create-func.js @@ -14,9 +14,8 @@ module.exports = new class ApiLiteralOrTypes { create(context) { return { ['TSDeclareFunction Identifier[name=/create.*/]']: (node) => { - var _a; const decl = node.parent; - if (((_a = decl.returnType) === null || _a === void 0 ? void 0 : _a.typeAnnotation.type) !== experimental_utils_1.AST_NODE_TYPES.TSTypeReference) { + if (decl.returnType?.typeAnnotation.type !== experimental_utils_1.AST_NODE_TYPES.TSTypeReference) { return; } if (decl.returnType.typeAnnotation.typeName.type !== experimental_utils_1.AST_NODE_TYPES.Identifier) { diff --git a/build/lib/eslint/vscode-dts-event-naming.js b/build/lib/eslint/vscode-dts-event-naming.js index a1a1c1097f1..1e376cca734 100644 --- a/build/lib/eslint/vscode-dts-event-naming.js +++ b/build/lib/eslint/vscode-dts-event-naming.js @@ -25,8 +25,7 @@ module.exports = new (_a = class ApiEventNaming { const verbs = new Set(config.verbs); return { ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node) => { - var _a, _b; - const def = (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.parent; + const def = node.parent?.parent?.parent; const ident = this.getIdent(def); if (!ident) { // event on unknown structure... diff --git a/build/lib/eslint/vscode-dts-provider-naming.js b/build/lib/eslint/vscode-dts-provider-naming.js index 924c26ecbb3..0d94a8a9223 100644 --- a/build/lib/eslint/vscode-dts-provider-naming.js +++ b/build/lib/eslint/vscode-dts-provider-naming.js @@ -17,8 +17,7 @@ module.exports = new (_a = class ApiProviderNaming { const allowed = new Set(config.allowed); return { ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node) => { - var _a; - const interfaceName = ((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent).id.name; + const interfaceName = node.parent?.parent.id.name; if (allowed.has(interfaceName)) { // allowed return; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 3707c802df7..99ebc6dee2b 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -115,7 +115,10 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { result.emit('error', compilation.warnings.join('\n')); } }; - const webpackConfig = Object.assign(Object.assign({}, require(webpackConfigPath)), { mode: 'production' }); + const webpackConfig = { + ...require(webpackConfigPath), + ...{ mode: 'production' } + }; const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); return webpackGulp(webpackConfig, webpack, webpackDone) .pipe(es.through(function (data) { @@ -350,16 +353,13 @@ function translatePackageJSON(packageJSON, packageNLSPath) { } exports.translatePackageJSON = translatePackageJSON; const extensionsPath = path.join(root, 'extensions'); -// Additional projects to webpack. These typically build code for webviews -const webpackMediaConfigFiles = [ - 'markdown-language-features/webpack.config.js', - 'simple-browser/webpack.config.js', -]; // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ - 'markdown-language-features/esbuild.js', + 'markdown-language-features/esbuild-notebook.js', + 'markdown-language-features/esbuild-preview.js', 'markdown-math/esbuild.js', - 'notebook-renderers/esbuild.js' + 'notebook-renderers/esbuild.js', + 'simple-browser/esbuild-preview.js', ]; async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { const webpack = require('webpack'); @@ -411,7 +411,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { reject(); } else { - reporter(stats === null || stats === void 0 ? void 0 : stats.toJson()); + reporter(stats?.toJson()); } }); } @@ -422,7 +422,7 @@ async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { reject(); } else { - reporter(stats === null || stats === void 0 ? void 0 : stats.toJson()); + reporter(stats?.toJson()); resolve(); } }); @@ -465,17 +465,9 @@ async function esbuildExtensions(taskName, isWatch, scripts) { return Promise.all(tasks); } async function buildExtensionMedia(isWatch, outputRoot) { - return Promise.all([ - webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { - return { - configPath: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }; - })), - esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }))), - ]); + return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))); } exports.buildExtensionMedia = buildExtensionMedia; diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 7303b3b151a..aeea8e68852 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -434,17 +434,13 @@ export function translatePackageJSON(packageJSON: string, packageNLSPath: string const extensionsPath = path.join(root, 'extensions'); -// Additional projects to webpack. These typically build code for webviews -const webpackMediaConfigFiles = [ - 'markdown-language-features/webpack.config.js', - 'simple-browser/webpack.config.js', -]; - // Additional projects to run esbuild on. These typically build code for webviews const esbuildMediaScripts = [ - 'markdown-language-features/esbuild.js', + 'markdown-language-features/esbuild-notebook.js', + 'markdown-language-features/esbuild-preview.js', 'markdown-math/esbuild.js', - 'notebook-renderers/esbuild.js' + 'notebook-renderers/esbuild.js', + 'simple-browser/esbuild-preview.js', ]; export async function webpackExtensions(taskName: string, isWatch: boolean, webpackConfigLocations: { configPath: string; outputRoot?: string }[]) { @@ -554,16 +550,8 @@ async function esbuildExtensions(taskName: string, isWatch: boolean, scripts: { } export async function buildExtensionMedia(isWatch: boolean, outputRoot?: string) { - return Promise.all([ - webpackExtensions('webpacking extension media', isWatch, webpackMediaConfigFiles.map(p => { - return { - configPath: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }; - })), - esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path.join(extensionsPath, p), - outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined - }))), - ]); + return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ + script: path.join(extensionsPath, p), + outputRoot: outputRoot ? path.join(root, outputRoot, path.dirname(p)) : undefined + }))); } diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 305653d159c..d273101ee99 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -430,6 +430,10 @@ "name": "vs/workbench/contrib/timeline", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/localHistory", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/authentication", "project": "vscode-workbench" diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 658ccfc516c..0dac1eed7ff 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -33,6 +33,7 @@ const CORE_TYPES = [ 'info', 'warn', 'error', + 'trace', 'group', 'groupEnd', 'table', @@ -53,6 +54,21 @@ const CORE_TYPES = [ 'trimLeft', 'trimRight', 'queueMicrotask', + 'Array', + 'Uint8Array', + 'Uint16Array', + 'Uint32Array', + 'Int8Array', + 'Int16Array', + 'Int32Array', + 'Float32Array', + 'Float64Array', + 'Uint8ClampedArray', + 'BigUint64Array', + 'BigInt64Array', + 'btoa', + 'atob', + 'AbortSignal', 'MessageChannel', 'MessagePort' ]; @@ -145,6 +161,9 @@ const RULES = [ target: '**/vs/**/browser/**', allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, + allowedDefinitions: [ + '@types/node/stream/consumers.d.ts' // node.js started to duplicate types from lib.dom.d.ts so we have to account for that + ], disallowedDefinitions: [ '@types/node' // no node.js ] @@ -209,15 +228,14 @@ let hasErrors = false; function checkFile(program, sourceFile, rule) { checkNode(sourceFile); function checkNode(node) { - var _a, _b; if (node.kind !== ts.SyntaxKind.Identifier) { return ts.forEachChild(node, checkNode); // recurse down } const text = node.getText(sourceFile); - if ((_a = rule.allowedTypes) === null || _a === void 0 ? void 0 : _a.some(allowed => allowed === text)) { + if (rule.allowedTypes?.some(allowed => allowed === text)) { return; // override } - if ((_b = rule.disallowedTypes) === null || _b === void 0 ? void 0 : _b.some(disallowed => disallowed === text)) { + if (rule.disallowedTypes?.some(disallowed => disallowed === text)) { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); console.log(`[build/lib/layersChecker.ts]: Reference to '${text}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); hasErrors = true; diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 6a860200085..a3800221719 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -34,6 +34,7 @@ const CORE_TYPES = [ 'info', 'warn', 'error', + 'trace', 'group', 'groupEnd', 'table', @@ -54,6 +55,21 @@ const CORE_TYPES = [ 'trimLeft', 'trimRight', 'queueMicrotask', + 'Array', + 'Uint8Array', + 'Uint16Array', + 'Uint32Array', + 'Int8Array', + 'Int16Array', + 'Int32Array', + 'Float32Array', + 'Float64Array', + 'Uint8ClampedArray', + 'BigUint64Array', + 'BigInt64Array', + 'btoa', + 'atob', + 'AbortSignal', 'MessageChannel', 'MessagePort' ]; @@ -158,6 +174,9 @@ const RULES = [ target: '**/vs/**/browser/**', allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, + allowedDefinitions: [ + '@types/node/stream/consumers.d.ts' // node.js started to duplicate types from lib.dom.d.ts so we have to account for that + ], disallowedDefinitions: [ '@types/node' // no node.js ] diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js index 98234633a2a..d708d6346b3 100644 --- a/build/lib/monaco-api.js +++ b/build/lib/monaco-api.js @@ -597,6 +597,12 @@ class TypeScriptLanguageServiceHost { isDefaultLibFileName(fileName) { return fileName === this.getDefaultLibFileName(this._compilerOptions); } + readFile(path, _encoding) { + return this._files[path] || this._libs[path]; + } + fileExists(path) { + return path in this._files || path in this._libs; + } } function execute() { let r = run3(new DeclarationResolver(new FSProvider())); diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts index 69deaf7471c..e6a06d21ea2 100644 --- a/build/lib/monaco-api.ts +++ b/build/lib/monaco-api.ts @@ -593,7 +593,7 @@ class CacheEntry { constructor( public readonly sourceFile: ts.SourceFile, public readonly mtime: number - ) {} + ) { } } export class DeclarationResolver { @@ -723,6 +723,12 @@ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { isDefaultLibFileName(fileName: string): boolean { return fileName === this.getDefaultLibFileName(this._compilerOptions); } + readFile(path: string, _encoding?: string): string | undefined { + return this._files[path] || this._libs[path]; + } + fileExists(path: string): boolean { + return path in this._files || path in this._libs; + } } export function execute(): IMonacoDeclarationResult { diff --git a/build/lib/nls.js b/build/lib/nls.js index 8930b31e96c..7dd43a78207 100644 --- a/build/lib/nls.js +++ b/build/lib/nls.js @@ -107,6 +107,15 @@ var _nls; this.file = ts.ScriptSnapshot.fromString(contents); this.lib = ts.ScriptSnapshot.fromString(''); } + readFile(path, _encoding) { + if (path === this.filename) { + return this.file.getText(0, this.file.getLength()); + } + return undefined; + } + fileExists(path) { + return path === this.filename; + } } function isCallExpressionWithinTextSpanCollectStep(ts, textSpan, node) { if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { diff --git a/build/lib/nls.ts b/build/lib/nls.ts index cd184ad9852..00f153acfc8 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -59,7 +59,7 @@ function template(lines: string[]): string { return `/*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -define([], [${ wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; +define([], [${wrap + lines.map(l => indent + l).join(',\n') + wrap}]);`; } /** @@ -161,6 +161,16 @@ module _nls { getScriptSnapshot = (name: string) => name === this.filename ? this.file : this.lib; getCurrentDirectory = () => ''; getDefaultLibFileName = () => 'lib.d.ts'; + + readFile(path: string, _encoding?: string): string | undefined { + if (path === this.filename) { + return this.file.getText(0, this.file.getLength()); + } + return undefined; + } + fileExists(path: string): boolean { + return path === this.filename; + } } function isCallExpressionWithinTextSpanCollectStep(ts: typeof import('typescript'), textSpan: ts.TextSpan, node: ts.Node): CollectStepResult { diff --git a/build/lib/node.js b/build/lib/node.js index e727aff8323..588962fa5bd 100644 --- a/build/lib/node.js +++ b/build/lib/node.js @@ -11,7 +11,7 @@ const yarnrcPath = path.join(root, 'remote', '.yarnrc'); const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)[1]; const platform = process.platform; -const arch = platform === 'darwin' ? 'x64' : process.arch; +const arch = process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); console.log(nodePath); diff --git a/build/lib/node.ts b/build/lib/node.ts index 6ac45ebb1f8..d1b0039b022 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -12,7 +12,7 @@ const yarnrc = fs.readFileSync(yarnrcPath, 'utf8'); const version = /^target\s+"([^"]+)"$/m.exec(yarnrc)![1]; const platform = process.platform; -const arch = platform === 'darwin' ? 'x64' : process.arch; +const arch = process.arch; const node = platform === 'win32' ? 'node.exe' : 'node'; const nodePath = path.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js index b1ecd53b5d9..ba8ba1c0382 100644 --- a/build/lib/preLaunch.js +++ b/build/lib/preLaunch.js @@ -13,7 +13,7 @@ const rootDir = path.resolve(__dirname, '..', '..'); function runProcess(command, args = []) { return new Promise((resolve, reject) => { const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env }); - child.on('exit', err => !err ? resolve() : process.exit(err !== null && err !== void 0 ? err : 1)); + child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); child.on('error', reject); }); } @@ -22,7 +22,7 @@ async function exists(subdir) { await fs_1.promises.stat(path.join(rootDir, subdir)); return true; } - catch (_a) { + catch { return false; } } diff --git a/build/lib/standalone.js b/build/lib/standalone.js index 91a693ce233..f2116eb159e 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -27,7 +27,6 @@ function writeFile(filePath, contents) { fs.writeFileSync(filePath, contents); } function extractEditor(options) { - var _a; const ts = require('typescript'); const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions; @@ -49,7 +48,7 @@ function extractEditor(options) { // Take the extra included .d.ts files from `tsconfig.monaco.json` options.typings = tsConfig.include.filter(includedFile => /\.d\.ts$/.test(includedFile)); // Add extra .d.ts files from `node_modules/@types/` - if (Array.isArray((_a = options.compilerOptions) === null || _a === void 0 ? void 0 : _a.types)) { + if (Array.isArray(options.compilerOptions?.types)) { options.compilerOptions.types.forEach((type) => { options.typings.push(`../node_modules/@types/${type}/index.d.ts`); }); diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 1a2da702fda..008eb27aca2 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -16,11 +16,11 @@ var ShakeLevel; })(ShakeLevel = exports.ShakeLevel || (exports.ShakeLevel = {})); function toStringShakeLevel(shakeLevel) { switch (shakeLevel) { - case 0 /* Files */: + case 0 /* ShakeLevel.Files */: return 'Files (0)'; - case 1 /* InnerFile */: + case 1 /* ShakeLevel.InnerFile */: return 'InnerFile (1)'; - case 2 /* ClassMembers */: + case 2 /* ShakeLevel.ClassMembers */: return 'ClassMembers (2)'; } } @@ -205,6 +205,12 @@ class TypeScriptLanguageServiceHost { isDefaultLibFileName(fileName) { return fileName === this.getDefaultLibFileName(this._compilerOptions); } + readFile(path, _encoding) { + return this._files[path] || this._libs[path]; + } + fileExists(path) { + return path in this._files || path in this._libs; + } } //#endregion //#region Tree Shaking @@ -215,7 +221,7 @@ var NodeColor; NodeColor[NodeColor["Black"] = 2] = "Black"; })(NodeColor || (NodeColor = {})); function getColor(node) { - return node.$$$color || 0 /* White */; + return node.$$$color || 0 /* NodeColor.White */; } function setColor(node, color) { node.$$$color = color; @@ -223,7 +229,7 @@ function setColor(node, color) { function nodeOrParentIsBlack(node) { while (node) { const color = getColor(node); - if (color === 2 /* Black */) { + if (color === 2 /* NodeColor.Black */) { return true; } node = node.parent; @@ -231,7 +237,7 @@ function nodeOrParentIsBlack(node) { return false; } function nodeOrChildIsBlack(node) { - if (getColor(node) === 2 /* Black */) { + if (getColor(node) === 2 /* NodeColor.Black */) { return true; } for (const child of node.getChildren()) { @@ -295,10 +301,10 @@ function markNodes(ts, languageService, options) { if (!program) { throw new Error('Could not get program from language service'); } - if (options.shakeLevel === 0 /* Files */) { + if (options.shakeLevel === 0 /* ShakeLevel.Files */) { // Mark all source files Black program.getSourceFiles().forEach((sourceFile) => { - setColor(sourceFile, 2 /* Black */); + setColor(sourceFile, 2 /* NodeColor.Black */); }); return; } @@ -310,7 +316,7 @@ function markNodes(ts, languageService, options) { sourceFile.forEachChild((node) => { if (ts.isImportDeclaration(node)) { if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); enqueueImport(node, node.moduleSpecifier.text); } return; @@ -318,7 +324,7 @@ function markNodes(ts, languageService, options) { if (ts.isExportDeclaration(node)) { if (!node.exportClause && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { // export * from "foo"; - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); enqueueImport(node, node.moduleSpecifier.text); } if (node.exportClause && ts.isNamedExports(node.exportClause)) { @@ -346,21 +352,21 @@ function markNodes(ts, languageService, options) { }); } function enqueue_gray(node) { - if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* Gray */) { + if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* NodeColor.Gray */) { return; } - setColor(node, 1 /* Gray */); + setColor(node, 1 /* NodeColor.Gray */); gray_queue.push(node); } function enqueue_black(node) { const previousColor = getColor(node); - if (previousColor === 2 /* Black */) { + if (previousColor === 2 /* NodeColor.Black */) { return; } - if (previousColor === 1 /* Gray */) { + if (previousColor === 1 /* NodeColor.Gray */) { // remove from gray queue gray_queue.splice(gray_queue.indexOf(node), 1); - setColor(node, 0 /* White */); + setColor(node, 0 /* NodeColor.White */); // add to black queue enqueue_black(node); // move from one queue to the other @@ -373,7 +379,7 @@ function markNodes(ts, languageService, options) { } const fileName = node.getSourceFile().fileName; if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); return; } const sourceFile = node.getSourceFile(); @@ -384,9 +390,9 @@ function markNodes(ts, languageService, options) { if (ts.isSourceFile(node)) { return; } - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); black_queue.push(node); - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { + if (options.shakeLevel === 2 /* ShakeLevel.ClassMembers */ && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); if (references) { for (let i = 0, len = references.length; i < len; i++) { @@ -447,7 +453,7 @@ function markNodes(ts, languageService, options) { if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { gray_queue.splice(i, 1); black_queue.push(node); - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); i--; } } @@ -463,7 +469,7 @@ function markNodes(ts, languageService, options) { const loop = (node) => { const [symbol, symbolImportNode] = getRealNodeSymbol(ts, checker, node); if (symbolImportNode) { - setColor(symbolImportNode, 2 /* Black */); + setColor(symbolImportNode, 2 /* NodeColor.Black */); } if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { for (let i = 0, len = symbol.declarations.length; i < len; i++) { @@ -473,7 +479,7 @@ function markNodes(ts, languageService, options) { // (they can be the declaration of a module import) continue; } - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { + if (options.shakeLevel === 2 /* ShakeLevel.ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { enqueue_black(declaration.name); for (let j = 0; j < declaration.members.length; j++) { const member = declaration.members[j]; @@ -523,7 +529,7 @@ function markNodes(ts, languageService, options) { const aliased = checker.getAliasedSymbol(symbol); if (aliased.declarations && aliased.declarations.length > 0) { if (nodeOrParentIsBlack(aliased.declarations[0]) || nodeOrChildIsBlack(aliased.declarations[0])) { - setColor(node, 2 /* Black */); + setColor(node, 2 /* NodeColor.Black */); } } } @@ -570,7 +576,7 @@ function generateResult(ts, languageService, shakeLevel) { result += data; } function writeMarkedNodes(node) { - if (getColor(node) === 2 /* Black */) { + if (getColor(node) === 2 /* NodeColor.Black */) { return keep(node); } // Always keep certain top-level statements @@ -586,34 +592,34 @@ function generateResult(ts, languageService, shakeLevel) { if (ts.isImportDeclaration(node)) { if (node.importClause && node.importClause.namedBindings) { if (ts.isNamespaceImport(node.importClause.namedBindings)) { - if (getColor(node.importClause.namedBindings) === 2 /* Black */) { + if (getColor(node.importClause.namedBindings) === 2 /* NodeColor.Black */) { return keep(node); } } else { let survivingImports = []; for (const importNode of node.importClause.namedBindings.elements) { - if (getColor(importNode) === 2 /* Black */) { + if (getColor(importNode) === 2 /* NodeColor.Black */) { survivingImports.push(importNode.getFullText(sourceFile)); } } const leadingTriviaWidth = node.getLeadingTriviaWidth(); const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); if (survivingImports.length > 0) { - if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* Black */) { + if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* NodeColor.Black */) { return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); } return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); } else { - if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* Black */) { + if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* NodeColor.Black */) { return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`); } } } } else { - if (node.importClause && getColor(node.importClause) === 2 /* Black */) { + if (node.importClause && getColor(node.importClause) === 2 /* NodeColor.Black */) { return keep(node); } } @@ -622,7 +628,7 @@ function generateResult(ts, languageService, shakeLevel) { if (node.exportClause && node.moduleSpecifier && ts.isNamedExports(node.exportClause)) { let survivingExports = []; for (const exportSpecifier of node.exportClause.elements) { - if (getColor(exportSpecifier) === 2 /* Black */) { + if (getColor(exportSpecifier) === 2 /* NodeColor.Black */) { survivingExports.push(exportSpecifier.getFullText(sourceFile)); } } @@ -633,11 +639,11 @@ function generateResult(ts, languageService, shakeLevel) { } } } - if (shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { + if (shakeLevel === 2 /* ShakeLevel.ClassMembers */ && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { let toWrite = node.getFullText(); for (let i = node.members.length - 1; i >= 0; i--) { const member = node.members[i]; - if (getColor(member) === 2 /* Black */ || !member.name) { + if (getColor(member) === 2 /* NodeColor.Black */ || !member.name) { // keep method continue; } @@ -653,7 +659,7 @@ function generateResult(ts, languageService, shakeLevel) { } node.forEachChild(writeMarkedNodes); } - if (getColor(sourceFile) !== 2 /* Black */) { + if (getColor(sourceFile) !== 2 /* NodeColor.Black */) { if (!nodeOrChildIsBlack(sourceFile)) { // none of the elements are reachable => don't write this file at all! return; diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 9ea3a2e0c48..113340285a8 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -284,6 +284,12 @@ class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { isDefaultLibFileName(fileName: string): boolean { return fileName === this.getDefaultLibFileName(this._compilerOptions); } + readFile(path: string, _encoding?: string): string | undefined { + return this._files[path] || this._libs[path]; + } + fileExists(path: string): boolean { + return path in this._files || path in this._libs; + } } //#endregion diff --git a/build/lib/util.js b/build/lib/util.js index e1fbc80f5d8..4efe2b3a046 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -12,8 +12,8 @@ const rename = require("gulp-rename"); const path = require("path"); const fs = require("fs"); const _rimraf = require("rimraf"); -const git = require("./git"); const VinylFile = require("vinyl"); +const git = require("./git"); const root = path.dirname(path.dirname(__dirname)); const NoCancellationToken = { isCancellationRequested: () => false }; function incremental(streamProvider, initial, supportsCancellation) { @@ -254,8 +254,8 @@ function ensureDir(dirPath) { } exports.ensureDir = ensureDir; function getVersion(root) { - let version = process.env['BUILD_SOURCEVERSION']; - if (!version || !/^[0-9a-f]{40}$/i.test(version)) { + let version = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; + if (!version || !/^[0-9a-f]{40}$/i.test(version.trim())) { version = git.getVersion(root); } return version; @@ -304,7 +304,6 @@ function getElectronVersion() { } exports.getElectronVersion = getElectronVersion; function acquireWebNodePaths() { - var _a; const root = path.join(__dirname, '..', '..'); const webPackageJSON = path.join(root, '/remote/web', 'package.json'); const webPackages = JSON.parse(fs.readFileSync(webPackageJSON, 'utf8')).dependencies; @@ -312,7 +311,7 @@ function acquireWebNodePaths() { for (const key of Object.keys(webPackages)) { const packageJSON = path.join(root, 'node_modules', key, 'package.json'); const packageData = JSON.parse(fs.readFileSync(packageJSON, 'utf8')); - let entryPoint = (_a = packageData.browser) !== null && _a !== void 0 ? _a : packageData.main; + let entryPoint = packageData.browser ?? packageData.main; // On rare cases a package doesn't have an entrypoint so we assume it has a dist folder with a min.js if (!entryPoint) { // TODO @lramos15 remove this when jschardet adds an entrypoint so we can warn on all packages w/out entrypoint diff --git a/build/lib/util.ts b/build/lib/util.ts index 7b0fe60eb99..e1cb4e70be0 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -13,10 +13,10 @@ import * as _ from 'underscore'; import * as path from 'path'; import * as fs from 'fs'; import * as _rimraf from 'rimraf'; -import * as git from './git'; import * as VinylFile from 'vinyl'; import { ThroughStream } from 'through'; import * as sm from 'source-map'; +import * as git from './git'; const root = path.dirname(path.dirname(__dirname)); @@ -320,9 +320,9 @@ export function ensureDir(dirPath: string): void { } export function getVersion(root: string): string | undefined { - let version = process.env['BUILD_SOURCEVERSION']; + let version = process.env['VSCODE_DISTRO_COMMIT'] || process.env['BUILD_SOURCEVERSION']; - if (!version || !/^[0-9a-f]{40}$/i.test(version)) { + if (!version || !/^[0-9a-f]{40}$/i.test(version.trim())) { version = git.getVersion(root); } diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js new file mode 100644 index 00000000000..f72b442670a --- /dev/null +++ b/build/linux/rpm/dep-lists.js @@ -0,0 +1,31 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.bundledDeps = exports.additionalDeps = void 0; +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/additional_deps +// Additional dependencies not in the rpm find-requires output. +exports.additionalDeps = [ + 'ca-certificates', + 'libgtk-3.so.0()(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'libvulkan.so.1()(64bit)', + 'libcurl.so.4()(64bit)', + 'xdg-utils' // OS integration +]; +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80 +// and the Linux Archive build +// Shared library dependencies that we already bundle. +exports.bundledDeps = [ + 'libEGL.so', + 'libGLESv2.so', + 'libvulkan.so.1', + 'swiftshader_libEGL.so', + 'swiftshader_libGLESv2.so', + 'libvk_swiftshader.so', + 'libffmpeg.so' +]; diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts new file mode 100644 index 00000000000..884f6825fbd --- /dev/null +++ b/build/linux/rpm/dep-lists.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/additional_deps +// Additional dependencies not in the rpm find-requires output. +export const additionalDeps = [ + 'ca-certificates', // Make sure users have SSL certificates. + 'libgtk-3.so.0()(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'libvulkan.so.1()(64bit)', + 'libcurl.so.4()(64bit)', + 'xdg-utils' // OS integration +]; + +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/98.0.4758.109:chrome/installer/linux/BUILD.gn;l=64-80 +// and the Linux Archive build +// Shared library dependencies that we already bundle. +export const bundledDeps = [ + 'libEGL.so', + 'libGLESv2.so', + 'libvulkan.so.1', + 'swiftshader_libEGL.so', + 'swiftshader_libGLESv2.so', + 'libvk_swiftshader.so', + 'libffmpeg.so' +]; diff --git a/build/linux/rpm/dependencies-generator.js b/build/linux/rpm/dependencies-generator.js new file mode 100644 index 00000000000..ddeefe3c28d --- /dev/null +++ b/build/linux/rpm/dependencies-generator.js @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getDependencies = void 0; +const child_process_1 = require("child_process"); +const fs_1 = require("fs"); +const path = require("path"); +const dep_lists_1 = require("./dep-lists"); +function getDependencies(buildDir, applicationName) { + // Get the files for which we want to find dependencies. + const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); + const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']); + if (findResult.status) { + console.error('Error finding files:'); + console.error(findResult.stderr.toString()); + return []; + } + const files = findResult.stdout.toString().trimEnd().split('\n'); + const appPath = path.join(buildDir, applicationName); + files.push(appPath); + // Add chrome sandbox and crashpad handler. + files.push(path.join(buildDir, 'chrome-sandbox')); + files.push(path.join(buildDir, 'chrome_crashpad_handler')); + // Generate the dependencies. + const dependencies = files.map((file) => calculatePackageDeps(file)); + // Add additional dependencies. + const additionalDepsSet = new Set(dep_lists_1.additionalDeps); + dependencies.push(additionalDepsSet); + // Merge all the dependencies. + const mergedDependencies = mergePackageDeps(dependencies); + let sortedDependencies = []; + for (const dependency of mergedDependencies) { + sortedDependencies.push(dependency); + } + sortedDependencies.sort(); + // Exclude bundled dependencies + sortedDependencies = sortedDependencies.filter(dependency => { + return !dep_lists_1.bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); + }); + return sortedDependencies; +} +exports.getDependencies = getDependencies; +function calculatePackageDeps(binaryPath) { + try { + if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) { + throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); + } + } + catch (e) { + // The package might not exist. Don't re-throw the error here. + console.error('Tried to stat ' + binaryPath + ' but failed.'); + } + const findRequiresResult = (0, child_process_1.spawnSync)('/usr/lib/rpm/find-requires', { input: binaryPath + '\n' }); + if (findRequiresResult.status !== 0) { + throw new Error(`find-requires failed with exit code ${findRequiresResult.status}.\nstderr: ${findRequiresResult.stderr}`); + } + const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); + // we only need to use provides to check for newer dependencies + // const provides = readFileSync('dist_package_provides.json'); + // const jsonProvides = JSON.parse(provides.toString('utf-8')); + return requires; +} +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py +function mergePackageDeps(inputDeps) { + const requires = new Set(); + for (const depSet of inputDeps) { + for (const dep of depSet) { + const trimmedDependency = dep.trim(); + if (trimmedDependency.length && !trimmedDependency.startsWith('#')) { + requires.add(trimmedDependency); + } + } + } + return requires; +} diff --git a/build/linux/rpm/dependencies-generator.ts b/build/linux/rpm/dependencies-generator.ts new file mode 100644 index 00000000000..97d313fe570 --- /dev/null +++ b/build/linux/rpm/dependencies-generator.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { spawnSync } from 'child_process'; +import { constants, statSync } from 'fs'; +import path = require('path'); +import { additionalDeps, bundledDeps } from './dep-lists'; + +export function getDependencies(buildDir: string, applicationName: string): string[] { + // Get the files for which we want to find dependencies. + const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); + const findResult = spawnSync('find', [nativeModulesPath, '-name', '*.node']); + if (findResult.status) { + console.error('Error finding files:'); + console.error(findResult.stderr.toString()); + return []; + } + + const files = findResult.stdout.toString().trimEnd().split('\n'); + + const appPath = path.join(buildDir, applicationName); + files.push(appPath); + + // Add chrome sandbox and crashpad handler. + files.push(path.join(buildDir, 'chrome-sandbox')); + files.push(path.join(buildDir, 'chrome_crashpad_handler')); + + // Generate the dependencies. + const dependencies: Set[] = files.map((file) => calculatePackageDeps(file)); + + // Add additional dependencies. + const additionalDepsSet = new Set(additionalDeps); + dependencies.push(additionalDepsSet); + + // Merge all the dependencies. + const mergedDependencies = mergePackageDeps(dependencies); + let sortedDependencies: string[] = []; + for (const dependency of mergedDependencies) { + sortedDependencies.push(dependency); + } + sortedDependencies.sort(); + + // Exclude bundled dependencies + sortedDependencies = sortedDependencies.filter(dependency => { + return !bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); + }); + + return sortedDependencies; +} + +function calculatePackageDeps(binaryPath: string): Set { + try { + if (!(statSync(binaryPath).mode & constants.S_IXUSR)) { + throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); + } + } catch (e) { + // The package might not exist. Don't re-throw the error here. + console.error('Tried to stat ' + binaryPath + ' but failed.'); + } + + const findRequiresResult = spawnSync('/usr/lib/rpm/find-requires', { input: binaryPath + '\n' }); + if (findRequiresResult.status !== 0) { + throw new Error(`find-requires failed with exit code ${findRequiresResult.status}.\nstderr: ${findRequiresResult.stderr}`); + } + + const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); + + // we only need to use provides to check for newer dependencies + // const provides = readFileSync('dist_package_provides.json'); + // const jsonProvides = JSON.parse(provides.toString('utf-8')); + + return requires; +} + +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py +function mergePackageDeps(inputDeps: Set[]): Set { + const requires = new Set(); + for (const depSet of inputDeps) { + for (const dep of depSet) { + const trimmedDependency = dep.trim(); + if (trimmedDependency.length && !trimmedDependency.startsWith('#')) { + requires.add(trimmedDependency); + } + } + } + return requires; +} diff --git a/build/npm/jsconfig.json b/build/npm/jsconfig.json new file mode 100644 index 00000000000..41d18dab432 --- /dev/null +++ b/build/npm/jsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "lib": [ + "ES2020" + ], + "module": "node12", + "checkJs": true, + "noEmit": true + } +} diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 2433da29e86..4fad92fd60b 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - const cp = require('child_process'); const path = require('path'); const fs = require('fs'); @@ -21,7 +20,7 @@ function yarnInstall(location, opts) { const raw = process.env['npm_config_argv'] || '{}'; const argv = JSON.parse(raw); const original = argv.original || []; - const args = original.filter(arg => arg === '--ignore-optional' || arg === '--frozen-lockfile'); + const args = original.filter(arg => arg === '--ignore-optional' || arg === '--frozen-lockfile' || arg === '--check-files'); if (opts.ignoreEngines) { args.push('--ignore-engines'); delete opts.ignoreEngines; @@ -62,8 +61,10 @@ for (let dir of dirs) { if (process.env['VSCODE_REMOTE_CC']) { env['CC'] = process.env['VSCODE_REMOTE_CC']; } if (process.env['VSCODE_REMOTE_CXX']) { env['CXX'] = process.env['VSCODE_REMOTE_CXX']; } if (process.env['CXXFLAGS']) { delete env['CXXFLAGS']; } + if (process.env['CFLAGS']) { delete env['CFLAGS']; } if (process.env['LDFLAGS']) { delete env['LDFLAGS']; } if (process.env['VSCODE_REMOTE_NODE_GYP']) { env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } + opts = { env }; } else if (/^extensions\//.test(dir)) { opts = { ignoreEngines: true }; diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js index a93a91855c6..b217c7bc12d 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - let err = false; const nodeVersion = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.node); @@ -88,12 +87,11 @@ function hasSupportedVisualStudioVersion() { function installHeaders() { const yarn = 'yarn.cmd'; - const opts = { + const yarnResult = cp.spawnSync(yarn, ['install'], { env: process.env, cwd: path.join(__dirname, 'gyp'), stdio: 'inherit' - }; - const yarnResult = cp.spawnSync(yarn, ['install'], opts); + }); if (yarnResult.error || yarnResult.status !== 0) { console.error(`Installing node-gyp failed`); err = true; diff --git a/build/npm/update-all-grammars.js b/build/npm/update-all-grammars.mjs similarity index 59% rename from build/npm/update-all-grammars.js rename to build/npm/update-all-grammars.mjs index ec7d2e843a4..2da48570661 100644 --- a/build/npm/update-all-grammars.js +++ b/build/npm/update-all-grammars.mjs @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const cp = require('child_process'); -const fs = require('fs'); -const path = require('path'); +import { spawn as _spawn } from 'child_process'; +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; +import url from 'url' async function spawn(cmd, args, opts) { return new Promise((c, e) => { - const child = cp.spawn(cmd, args, { shell: true, stdio: 'inherit', env: process.env, ...opts }); + const child = _spawn(cmd, args, { shell: true, stdio: 'inherit', env: process.env, ...opts }); child.on('close', code => code === 0 ? c() : e(`Returned ${code}`)); }); } @@ -17,9 +18,9 @@ async function spawn(cmd, args, opts) { async function main() { await spawn('yarn', [], { cwd: 'extensions' }); - for (const extension of fs.readdirSync('extensions')) { + for (const extension of readdirSync('extensions')) { try { - let packageJSON = JSON.parse(fs.readFileSync(path.join('extensions', extension, 'package.json')).toString()); + let packageJSON = JSON.parse(readFileSync(join('extensions', extension, 'package.json')).toString()); if (!(packageJSON && packageJSON.scripts && packageJSON.scripts['update-grammar'])) { continue; } @@ -33,13 +34,13 @@ async function main() { // run integration tests if (process.platform === 'win32') { - cp.spawn('.\\scripts\\test-integration.bat', [], { env: process.env, stdio: 'inherit' }); + _spawn('.\\scripts\\test-integration.bat', [], { env: process.env, stdio: 'inherit' }); } else { - cp.spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); + _spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); } } -if (require.main === module) { +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { main().catch(err => { console.error(err); process.exit(1); diff --git a/build/npm/update-distro.js b/build/npm/update-distro.js deleted file mode 100644 index 947a4967d60..00000000000 --- a/build/npm/update-distro.js +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const cp = require('child_process'); -const path = require('path'); -const fs = require('fs'); - -const rootPath = path.dirname(path.dirname(path.dirname(__dirname))); -const vscodePath = path.join(rootPath, 'vscode'); -const distroPath = path.join(rootPath, 'vscode-distro'); -const commit = cp.execSync('git rev-parse HEAD', { cwd: distroPath, encoding: 'utf8' }).trim(); -const packageJsonPath = path.join(vscodePath, 'package.json'); -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - -packageJson.distro = commit; -fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); \ No newline at end of file diff --git a/build/npm/update-distro.mjs b/build/npm/update-distro.mjs new file mode 100644 index 00000000000..655d9f2c243 --- /dev/null +++ b/build/npm/update-distro.mjs @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { execSync } from 'child_process'; +import { join, resolve } from 'path'; +import { readFileSync, writeFileSync } from 'fs'; +import { fileURLToPath } from 'url'; + +const rootPath = resolve(fileURLToPath(import.meta.url), '..', '..', '..', '..'); +const vscodePath = join(rootPath, 'vscode'); +const distroPath = join(rootPath, 'vscode-distro'); +const commit = execSync('git rev-parse HEAD', { cwd: distroPath, encoding: 'utf8' }).trim(); +const packageJsonPath = join(vscodePath, 'package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + +packageJson.distro = commit; +writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js index 976776c6901..1a689a189ea 100644 --- a/build/npm/update-localization-extension.js +++ b/build/npm/update-localization-extension.js @@ -2,7 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - 'use strict'; let i18n = require("../lib/i18n"); diff --git a/build/package.json b/build/package.json index 456b7e87ed5..0d4fca81001 100644 --- a/build/package.json +++ b/build/package.json @@ -29,9 +29,8 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", - "@types/node": "14.x", + "@types/node": "16.x", "@types/p-limit": "^2.2.0", - "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", "@types/request": "^2.47.0", "@types/rimraf": "^2.0.4", @@ -59,7 +58,6 @@ "mime": "^1.4.1", "mkdirp": "^1.0.4", "p-limit": "^3.1.0", - "plist": "^3.0.1", "source-map": "0.6.1", "tmp": "^0.2.1", "vsce": "^1.100.0", diff --git a/build/tsconfig.build.json b/build/tsconfig.build.json index 2402a092886..0a00f2d0f48 100644 --- a/build/tsconfig.build.json +++ b/build/tsconfig.build.json @@ -2,6 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "allowJs": false, - "checkJs": false - } -} \ No newline at end of file + "checkJs": false, + "noEmit": false + }, + "include": [ + "**/*.ts" + ] +} diff --git a/build/tsconfig.json b/build/tsconfig.json index 10a454e94ad..de65bb698d3 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es2017", + "target": "es2020", + "lib": ["ES2020"], "module": "commonjs", "removeComments": false, "preserveConstEnums": true, @@ -11,16 +12,17 @@ // use the tsconfig.build.json for compiling which disable JavaScript // type checking so that JavaScript file are not transpiled "allowJs": true, - "checkJs": true, "strict": true, "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "noUnusedLocals": true, "noUnusedParameters": true, - "newLine": "lf" + "newLine": "lf", + "noEmit": true }, "include": [ - "**/*.ts" + "**/*.ts", + "**/*.js" ], "exclude": [ "node_modules/**" diff --git a/build/yarn.lock b/build/yarn.lock index 13d05a4352c..7c94ce506c3 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -507,10 +507,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/p-limit@^2.2.0": version "2.2.0" @@ -519,14 +519,6 @@ dependencies: p-limit "*" -"@types/plist@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" - integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== - dependencies: - "@types/node" "*" - xmlbuilder ">=11.0.1" - "@types/pump@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/pump/-/pump-1.0.1.tgz#ae8157cefef04d1a4d24c1cc91d403c2f5da5cd0" @@ -2972,11 +2964,6 @@ xml2js@^0.4.19, xml2js@^0.4.23: sax ">=0.6.0" xmlbuilder "~11.0.0" -xmlbuilder@>=11.0.1: - version "15.1.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" - integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== - xmlbuilder@^9.0.7: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" diff --git a/cgmanifest.json b/cgmanifest.json index 407e42c7d81..5304bf711b1 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "8a33e05d162c4f39afa2dcb150e8c2548aa4ccea" + "commitHash": "cbadbe267bec5c3c673d5a2f2fdef15541f5bfc1" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "91.0.4472.164" + "version": "98.0.4758.109" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "bd60e93357a118204ea238d94e7a9e4209d93062" + "commitHash": "40ecd5601193c316e62e9216e8a4259130686208" } }, "isOnlyProductionDependency": true, - "version": "14.16.0" + "version": "16.13.0" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "2727847acabd39933ccd95b92f2c12454c0a24e7" + "commitHash": "fdaee16db7c6c30afedfcedf8e60334d1fa80868" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "13.5.2" + "version": "17.1.2" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index a7b71cd05c8..0b501ce9d04 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -48,10 +48,10 @@ "extensions.json", "argv.json", "profiles.json", + "devcontainer.json", ".devcontainer.json" ], "filenamePatterns": [ - "**/.devcontainer/devcontainer.json", "**/User/snippets/*.json" ] } @@ -122,11 +122,11 @@ "url": "vscode://schemas/extensions" }, { - "fileMatch": "/.devcontainer/devcontainer.json", + "fileMatch": "devcontainer.json", "url": "./schemas/devContainer.schema.generated.json" }, { - "fileMatch": "/.devcontainer.json", + "fileMatch": ".devcontainer.json", "url": "./schemas/devContainer.schema.generated.json" }, { @@ -144,7 +144,7 @@ ] }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/configuration-editing/schemas/devContainer.schema.generated.json b/extensions/configuration-editing/schemas/devContainer.schema.generated.json index de9baa5060a..dfee9dea40c 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.generated.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.generated.json @@ -116,7 +116,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -403,37 +403,6 @@ } }, "additionalProperties": false - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - }, - "additionalProperties": false - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } }, "required": [ @@ -550,7 +519,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -837,37 +806,6 @@ } }, "additionalProperties": false - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - }, - "additionalProperties": false - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } }, "required": [ @@ -950,7 +888,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -1237,37 +1175,6 @@ } }, "additionalProperties": false - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - }, - "additionalProperties": false - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } }, "required": [ @@ -1324,7 +1231,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -1611,37 +1518,6 @@ } }, "additionalProperties": false - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - }, - "additionalProperties": false - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } }, "required": [ @@ -1663,7 +1539,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -1950,37 +1826,6 @@ } }, "additionalProperties": false - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - }, - "additionalProperties": false - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } }, "additionalProperties": false diff --git a/extensions/configuration-editing/schemas/devContainer.schema.src.json b/extensions/configuration-editing/schemas/devContainer.schema.src.json index 0fb2f7e0790..cea8472e8f6 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.src.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.src.json @@ -16,7 +16,7 @@ "description": "An array of extensions that should be installed into the container.", "items": { "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", + "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)((@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)|@prerelease)?$", "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." } }, @@ -306,36 +306,6 @@ } } ] - }, - "vscode": { - "type": "object", - "properties": { - "extensions": { - "type": "array", - "description": "An array of extensions that should be installed into the container.", - "items": { - "type": "string", - "pattern": "^([a-z0-9A-Z][a-z0-9A-Z-]*)\\.([a-z0-9A-Z][a-z0-9A-Z-]*)(@(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?)?$", - "errorMessage": "Expected format: '${publisher}.${name}' or '${publisher}.${name}@${version}'. Example: 'ms-dotnettools.csharp'." - } - }, - "settings": { - "$ref": "vscode://schemas/settings/machine", - "description": "Machine specific settings that should be copied into the container. These are only copied when connecting to the container for the first time, rebuilding the container then triggers it again." - }, - "devPort": { - "type": "integer", - "description": "The port VS Code can use to connect to its backend." - } - } - }, - "customizations": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": true - }, - "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations." } } }, diff --git a/extensions/configuration-editing/yarn.lock b/extensions/configuration-editing/yarn.lock index d4882d39e4b..f7ac959fc09 100644 --- a/extensions/configuration-editing/yarn.lock +++ b/extensions/configuration-editing/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== jsonc-parser@^2.2.1: version "2.2.1" diff --git a/extensions/css-language-features/client/src/cssClient.ts b/extensions/css-language-features/client/src/cssClient.ts index 8cf472d0a16..c71b89d08b7 100644 --- a/extensions/css-language-features/client/src/cssClient.ts +++ b/extensions/css-language-features/client/src/cssClient.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode'; -import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, CommonLanguageClient } from 'vscode-languageclient'; +import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList, FormattingOptions, workspace } from 'vscode'; +import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, CommonLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import { getCustomDataSource } from './customData'; import { RequestService, serveFileSystemRequests } from './requests'; @@ -22,12 +22,30 @@ export interface Runtime { fs?: RequestService; } +interface FormatterRegistration { + readonly languageId: string; + readonly settingId: string; + provider: Disposable | undefined; +} + +interface CSSFormatSettings { + selectorSeparatorNewline?: boolean; + newlineBetweenRules?: boolean; + spaceAroundSelectorSeparator?: boolean; +} + +const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['selectorSeparatorNewline', 'newlineBetweenRules', 'spaceAroundSelectorSeparator']; + export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { const customDataSource = getCustomDataSource(context.subscriptions); let documentSelector = ['css', 'scss', 'less']; + const formatterRegistrations: FormatterRegistration[] = documentSelector.map(languageId => ({ + languageId, settingId: `${languageId}.format.enable`, provider: undefined + })); + // Options to control the language client let clientOptions: LanguageClientOptions = { documentSelector, @@ -35,7 +53,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua configurationSection: ['css', 'scss', 'less'] }, initializationOptions: { - handledSchemas: ['file'] + handledSchemas: ['file'], + provideFormatter: false, // tell the server to not provide formatting capability + customCapabilities: { rangeFormatting: { editLimit: 10000 } } }, middleware: { provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { @@ -84,6 +104,13 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); }); + // manually register / deregister format provider based on the `css/less/scss.format.enable` setting avoiding issues with late registration. See #71652. + for (const registration of formatterRegistrations) { + updateFormatterRegistration(registration); + context.subscriptions.push({ dispose: () => registration.provider?.dispose() }); + context.subscriptions.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(registration.settingId) && updateFormatterRegistration(registration))); + } + serveFileSystemRequests(client, runtime); }); @@ -143,4 +170,47 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua }); } } + + function updateFormatterRegistration(registration: FormatterRegistration) { + const formatEnabled = workspace.getConfiguration().get(registration.settingId); + if (!formatEnabled && registration.provider) { + registration.provider.dispose(); + registration.provider = undefined; + } else if (formatEnabled && !registration.provider) { + registration.provider = languages.registerDocumentRangeFormattingEditProvider(registration.languageId, { + provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult { + const filesConfig = workspace.getConfiguration('files', document); + + const fileFormattingOptions = { + trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), + trimFinalNewlines: filesConfig.get('trimFinalNewlines'), + insertFinalNewline: filesConfig.get('insertFinalNewline'), + }; + const params: DocumentRangeFormattingParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + range: client.code2ProtocolConverter.asRange(range), + options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions) + }; + // add the css formatter options from the settings + const formatterSettings = workspace.getConfiguration(registration.languageId, document).get('format'); + if (formatterSettings) { + for (const key of cssFormatSettingKeys) { + const val = formatterSettings[key]; + if (val !== undefined) { + params.options[key] = val; + } + } + } + console.log(JSON.stringify(params.options)); + return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( + client.protocol2CodeConverter.asTextEdits, + (error) => { + client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); + return Promise.resolve([]); + } + ); + } + }); + } + } } diff --git a/extensions/css-language-features/client/src/node/nodeFs.ts b/extensions/css-language-features/client/src/node/nodeFs.ts index 0b572018884..e0d3f9e1403 100644 --- a/extensions/css-language-features/client/src/node/nodeFs.ts +++ b/extensions/css-language-features/client/src/node/nodeFs.ts @@ -14,7 +14,7 @@ export function getNodeFSRequestService(): RequestService { } } return { - getContent(location: string, encoding?: string) { + getContent(location: string, encoding?: BufferEncoding) { ensureFileUri(location); return new Promise((c, e) => { const uri = Uri.parse(location); diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index bd8db6d0259..df03d1f80b1 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -307,6 +307,30 @@ ], "default": "off", "description": "%css.trace.server.desc%" + }, + "css.format.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%css.format.enable.desc%" + }, + "css.format.selectorSeparatorNewline": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%css.format.selectorSeparatorNewline.desc%" + }, + "css.format.newlineBetweenRules": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%css.format.newlineBetweenRules.desc%" + }, + "css.format.spaceAroundSelectorSeparator": { + "type": "boolean", + "scope": "resource", + "default": false, + "markdownDescription": "%css.format.spaceAroundSelectorSeparator.desc%" } } }, @@ -563,6 +587,30 @@ ], "default": "warning", "description": "%scss.lint.unknownAtRules.desc%" + }, + "scss.format.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%scss.format.enable.desc%" + }, + "scss.format.selectorSeparatorNewline": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%scss.format.selectorSeparatorNewline.desc%" + }, + "scss.format.newlineBetweenRules": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%scss.format.newlineBetweenRules.desc%" + }, + "scss.format.spaceAroundSelectorSeparator": { + "type": "boolean", + "scope": "resource", + "default": false, + "markdownDescription": "%scss.format.spaceAroundSelectorSeparator.desc%" } } }, @@ -820,6 +868,30 @@ ], "default": "warning", "description": "%less.lint.unknownAtRules.desc%" + }, + "less.format.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%less.format.enable.desc%" + }, + "less.format.selectorSeparatorNewline": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%less.format.selectorSeparatorNewline.desc%" + }, + "less.format.newlineBetweenRules": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "%less.format.newlineBetweenRules.desc%" + }, + "less.format.spaceAroundSelectorSeparator": { + "type": "boolean", + "scope": "resource", + "default": false, + "markdownDescription": "%less.format.spaceAroundSelectorSeparator.desc%" } } } @@ -852,7 +924,7 @@ "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/css-language-features/package.nls.json b/extensions/css-language-features/package.nls.json index 727ba718d7f..914330a20fb 100644 --- a/extensions/css-language-features/package.nls.json +++ b/extensions/css-language-features/package.nls.json @@ -30,6 +30,10 @@ "css.validate.desc": "Enables or disables all validations.", "css.hover.documentation": "Show tag and attribute documentation in CSS hovers.", "css.hover.references": "Show references to MDN in CSS hovers.", + "css.format.enable.desc": "Enable/disable default LESS formatter.", + "css.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not", + "css.format.newlineBetweenRules.desc": "Add a new line after every css rule", + "css.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators", "less.title": "LESS", "less.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.", "less.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties.", @@ -57,6 +61,10 @@ "less.validate.desc": "Enables or disables all validations.", "less.hover.documentation": "Show tag and attribute documentation in LESS hovers.", "less.hover.references": "Show references to MDN in LESS hovers.", + "less.format.enable.desc": "Enable/disable default LESS formatter.", + "less.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not", + "less.format.newlineBetweenRules.desc": "Add a new line after every css rule", + "less.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators", "scss.title": "SCSS (Sass)", "scss.completion.triggerPropertyValueCompletion.desc": "By default, VS Code triggers property value completion after selecting a CSS property. Use this setting to disable this behavior.", "scss.completion.completePropertyWithSemicolon.desc": "Insert semicolon at end of line when completing CSS properties.", @@ -84,6 +92,10 @@ "scss.validate.desc": "Enables or disables all validations.", "scss.hover.documentation": "Show tag and attribute documentation in SCSS hovers.", "scss.hover.references": "Show references to MDN in SCSS hovers.", + "scss.format.enable.desc": "Enable/disable default LESS formatter.", + "scss.format.selectorSeparatorNewline.desc": "Separate selectors with newline or not", + "scss.format.newlineBetweenRules.desc": "Add a new line after every css rule", + "scss.format.spaceAroundSelectorSeparator.desc": "Ensure space around selector separators", "css.colorDecorators.enable.deprecationMessage": "The setting `css.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", "scss.colorDecorators.enable.deprecationMessage": "The setting `scss.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", "less.colorDecorators.enable.deprecationMessage": "The setting `less.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`." diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 63dee355923..4eb32c89145 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -10,13 +10,13 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^5.1.13", + "vscode-css-languageservice": "^5.2.0", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "14.x" + "@types/node": "16.x" }, "scripts": { "compile": "gulp compile-extension:css-language-features-server", diff --git a/extensions/css-language-features/server/src/cssServer.ts b/extensions/css-language-features/server/src/cssServer.ts index 89810d0ae41..40e05b13ab1 100644 --- a/extensions/css-language-features/server/src/cssServer.ts +++ b/extensions/css-language-features/server/src/cssServer.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { - Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable + Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable, TextDocumentIdentifier, Range, FormattingOptions, TextEdit } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; -import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position } from 'vscode-css-languageservice'; +import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position, CSSFormatConfiguration } from 'vscode-css-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { formatError, runSafeAsync } from './utils/runner'; import { getDocumentContext } from './utils/documentContext'; @@ -52,6 +52,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let scopedSettingsSupport = false; let foldingRangeLimit = Number.MAX_VALUE; let workspaceFolders: WorkspaceFolder[]; + let formatterMaxNumberOfEdits = Number.MAX_VALUE; let dataProvidersReady: Promise = Promise.resolve(); @@ -87,6 +88,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) const snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false); scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + formatterMaxNumberOfEdits = params.initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; languageServices.css = getCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); languageServices.scss = getSCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); @@ -107,7 +109,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) renameProvider: true, colorProvider: {}, foldingRangeProvider: true, - selectionRangeProvider: true + selectionRangeProvider: true, + documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentFormattingProvider: params.initializationOptions?.provideFormatter === true, }; return { capabilities }; }); @@ -367,6 +371,28 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); }); + async function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): Promise { + const document = documents.get(textDocument.uri); + if (document) { + console.log(JSON.stringify(options)); + const edits = getLanguageService(document).format(document, range ?? getFullRange(document), options as CSSFormatConfiguration); + if (edits.length > formatterMaxNumberOfEdits) { + const newText = TextDocument.applyEdits(document, edits); + return [TextEdit.replace(getFullRange(document), newText)]; + } + return edits; + } + return []; + } + + connection.onDocumentRangeFormatting((formatParams, token) => { + return runSafeAsync(runtime, () => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + }); + + connection.onDocumentFormatting((formatParams, token) => { + return runSafeAsync(runtime, () => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); + }); + connection.onNotification(CustomDataChangedNotification.type, updateDataProviders); // Listen on the connection @@ -374,4 +400,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } +function getFullRange(document: TextDocument): Range { + return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); +} + + diff --git a/extensions/css-language-features/server/src/node/nodeFs.ts b/extensions/css-language-features/server/src/node/nodeFs.ts index c72617e3af1..35e55622dc9 100644 --- a/extensions/css-language-features/server/src/node/nodeFs.ts +++ b/extensions/css-language-features/server/src/node/nodeFs.ts @@ -16,7 +16,7 @@ export function getNodeFSRequestService(): RequestService { } } return { - getContent(location: string, encoding?: string) { + getContent(location: string, encoding?: BufferEncoding) { ensureFileUri(location); return new Promise((c, e) => { const uri = Uri.parse(location); diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index ab6b163aee2..d0b969357eb 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -7,20 +7,20 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -vscode-css-languageservice@^5.1.13: - version "5.1.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.13.tgz#debc7c8368223b211a734cb7eb7789c586d3e2d9" - integrity sha512-FA0foqMzMmEoO0WJP+MjoD4dRERhKS+Ag+yBrtmWQDmw2OuZ1R/5FkvI/XdTkCpHmTD9VMczugpHRejQyTXCNQ== +vscode-css-languageservice@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.2.0.tgz#84fa95f8314c742080c09f623bf9f0727621eebf" + integrity sha512-FR5yDEfzbXJtYmZYrA7JWFcRSLHsJw3nv55XAmx7qdwRpFj9yy0ulKfN/NUUdiZW2jZU2fD/+Y4VJYPdafHDag== dependencies: - vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-textdocument "^1.0.4" vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" - vscode-uri "^3.0.2" + vscode-uri "^3.0.3" vscode-jsonrpc@6.0.0: version "6.0.0" @@ -35,10 +35,10 @@ vscode-languageserver-protocol@3.16.0: vscode-jsonrpc "6.0.0" vscode-languageserver-types "3.16.0" -vscode-languageserver-textdocument@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" - integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== +vscode-languageserver-textdocument@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157" + integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ== vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.16.0: version "3.16.0" @@ -57,11 +57,6 @@ vscode-nls@^5.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-uri@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" - integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== - vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 0875cabea4d..60ba7c2dcc3 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== balanced-match@^1.0.0: version "1.0.0" diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index b0c765fa2cc..82c9054c456 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -36,7 +36,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "prettier": { "printWidth": 100, diff --git a/extensions/debug-auto-launch/yarn.lock b/extensions/debug-auto-launch/yarn.lock index f7a30098ef4..22c406bc73f 100644 --- a/extensions/debug-auto-launch/yarn.lock +++ b/extensions/debug-auto-launch/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 64aa227aa53..29cee88c0c5 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -153,7 +153,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/debug-server-ready/yarn.lock b/extensions/debug-server-ready/yarn.lock index f7a30098ef4..22c406bc73f 100644 --- a/extensions/debug-server-ready/yarn.lock +++ b/extensions/debug-server-ready/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index 4b303046a26..659be1e0fce 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -500,7 +500,7 @@ "deps": "yarn add @vscode/emmet-helper" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "dependencies": { "@emmetio/abbreviation": "^2.2.0", diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index b62efc8c7a2..955f10670ee 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -60,10 +60,10 @@ resolved "https://registry.yarnpkg.com/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz#46cffea119a0a003312a21c2d9b5628cb5fcd442" integrity sha1-Rs/+oRmgoAMxKiHC2bVijLX81EI= -"@types/node@14.x": - version "14.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a" - integrity sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@vscode/emmet-helper@^2.3.0": version "2.8.4" diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index dbc18aa142b..abceee2bc7d 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -68,7 +68,7 @@ }, "devDependencies": { "@types/markdown-it": "0.0.2", - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/extension-editing/yarn.lock b/extensions/extension-editing/yarn.lock index 171c0baaa6f..ac87f5e2686 100644 --- a/extensions/extension-editing/yarn.lock +++ b/extensions/extension-editing/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660" integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA= -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/node@^6.0.46": version "6.0.78" diff --git a/extensions/fsharp/cgmanifest.json b/extensions/fsharp/cgmanifest.json index 29d6ae5aadd..207fed004fb 100644 --- a/extensions/fsharp/cgmanifest.json +++ b/extensions/fsharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "ionide/ionide-fsgrammar", "repositoryUrl": "https://github.com/ionide/ionide-fsgrammar", - "commitHash": "3311701c243d6ed5b080a2ee16ada51540a08c50" + "commitHash": "c6e04755d4d686653f2552183d9f102de489a0a5" } }, "license": "MIT", diff --git a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json index b4d2523b3e1..1a0661544f3 100644 --- a/extensions/fsharp/syntaxes/fsharp.tmLanguage.json +++ b/extensions/fsharp/syntaxes/fsharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/ionide/ionide-fsgrammar/commit/3311701c243d6ed5b080a2ee16ada51540a08c50", + "version": "https://github.com/ionide/ionide-fsgrammar/commit/c6e04755d4d686653f2552183d9f102de489a0a5", "name": "fsharp", "scopeName": "source.fsharp", "patterns": [ @@ -537,7 +537,7 @@ { "name": "comment.block.markdown.fsharp", "begin": "^\\s*(\\(\\*\\*(?!\\)))((?!\\*\\)).)*$", - "while": "^(?!\\s*(\\*)+\\)$)", + "while": "^(?!\\s*(\\*)+\\)\\s*$)", "beginCaptures": { "1": { "name": "comment.block.fsharp" diff --git a/extensions/git-base/package.json b/extensions/git-base/package.json index 3fc19c587ff..acc95e1316c 100644 --- a/extensions/git-base/package.json +++ b/extensions/git-base/package.json @@ -102,7 +102,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/git-base/yarn.lock b/extensions/git-base/yarn.lock index 22e3e094fdc..3244e2ea667 100644 --- a/extensions/git-base/yarn.lock +++ b/extensions/git-base/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.17.33" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.33.tgz#011ee28e38dc7aee1be032ceadf6332a0ab15b12" - integrity sha512-noEeJ06zbn3lOh4gqe2v7NMGS33jrulfNqYFDjjEbhpDEHR5VTxgYNQSBqBlJIsBJW3uEYDgD6kvMnrrhGzq8g== +"@types/node@16.x": + version "16.11.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.21.tgz#474d7589a30afcf5291f59bd49cca9ad171ffde4" + integrity sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A== vscode-nls@^4.0.0: version "4.1.2" diff --git a/extensions/git/package.json b/extensions/git/package.json index 73fd380e91d..4ca8f559663 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2282,7 +2282,8 @@ "defaults": { "light": "#587c0c", "dark": "#81b88b", - "highContrast": "#1b5225" + "highContrast": "#1b5225", + "highContrastLight": "#374e06" } }, { @@ -2291,7 +2292,8 @@ "defaults": { "light": "#895503", "dark": "#E2C08D", - "highContrast": "#E2C08D" + "highContrast": "#E2C08D", + "highContrastLight": "#895503" } }, { @@ -2300,7 +2302,8 @@ "defaults": { "light": "#ad0707", "dark": "#c74e39", - "highContrast": "#c74e39" + "highContrast": "#c74e39", + "highContrastLight": "#ad0707" } }, { @@ -2309,7 +2312,8 @@ "defaults": { "light": "#007100", "dark": "#73C991", - "highContrast": "#73C991" + "highContrast": "#73C991", + "highContrastLight": "#007100" } }, { @@ -2318,7 +2322,8 @@ "defaults": { "light": "#007100", "dark": "#73C991", - "highContrast": "#73C991" + "highContrast": "#73C991", + "highContrastLight": "#007100" } }, { @@ -2327,7 +2332,8 @@ "defaults": { "light": "#8E8E90", "dark": "#8C8C8C", - "highContrast": "#A7A8A9" + "highContrast": "#A7A8A9", + "highContrastLight": "#8e8e90" } }, { @@ -2336,7 +2342,8 @@ "defaults": { "light": "#895503", "dark": "#E2C08D", - "highContrast": "#E2C08D" + "highContrast": "#E2C08D", + "highContrastLight": "#895503" } }, { @@ -2345,7 +2352,8 @@ "defaults": { "light": "#ad0707", "dark": "#c74e39", - "highContrast": "#c74e39" + "highContrast": "#c74e39", + "highContrastLight": "#ad0707" } }, { @@ -2354,7 +2362,8 @@ "defaults": { "light": "#ad0707", "dark": "#e4676b", - "highContrast": "#c74e39" + "highContrast": "#c74e39", + "highContrastLight": "#ad0707" } }, { @@ -2363,7 +2372,8 @@ "defaults": { "light": "#1258a7", "dark": "#8db9e2", - "highContrast": "#8db9e2" + "highContrast": "#8db9e2", + "highContrastLight": "#1258a7" } } ], @@ -2458,7 +2468,7 @@ "byline": "^5.0.0", "file-type": "^7.2.0", "jschardet": "3.0.0", - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", "which": "^1.3.0" @@ -2467,7 +2477,7 @@ "@types/byline": "4.2.31", "@types/file-type": "^5.2.1", "@types/mocha": "^8.2.0", - "@types/node": "14.x", + "@types/node": "16.x", "@types/which": "^1.0.28" }, "repository": { diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 41be52f1878..32417c2a6d1 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -108,6 +108,10 @@ export class ApiRepository implements Repository { return this._repository.add(paths.map(p => Uri.file(p))); } + revert(paths: string[]) { + return this._repository.revert(paths.map(p => Uri.file(p))); + } + clean(paths: string[]) { return this._repository.clean(paths.map(p => Uri.file(p))); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index c1053676969..7ba58a0e607 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -173,6 +173,7 @@ export interface Repository { getCommit(ref: string): Promise; add(paths: string[]): Promise; + revert(paths: string[]): Promise; clean(paths: string[]): Promise; apply(patch: string, reverse?: boolean): Promise; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index b4d26d99a10..39ed6847423 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -7,6 +7,7 @@ import { promises as fs, exists, realpath } from 'fs'; import * as path from 'path'; import * as os from 'os'; import * as cp from 'child_process'; +import { fileURLToPath } from 'url'; import * as which from 'which'; import { EventEmitter } from 'events'; import * as iconv from '@vscode/iconv-lite-umd'; @@ -605,13 +606,27 @@ export class Git { GIT_PAGER: 'cat' }); - if (options.cwd) { - options.cwd = sanitizePath(options.cwd); + const cwd = this.getCwd(options); + if (cwd) { + options.cwd = sanitizePath(cwd); } return cp.spawn(this.path, args, options); } + private getCwd(options: SpawnOptions): string | undefined { + const cwd = options.cwd; + if (typeof cwd === 'undefined' || typeof cwd === 'string') { + return cwd; + } + + if (cwd.protocol === 'file:') { + return fileURLToPath(cwd); + } + + return undefined; + } + private log(output: string): void { this._onOutput.emit('log', output); } @@ -2028,6 +2043,7 @@ export class Repository { if (branchName.startsWith('refs/heads/')) { branchName = branchName.substring(11); + const index = upstream.indexOf('/'); let ahead; let behind; @@ -2040,8 +2056,8 @@ export class Repository { type: RefType.Head, name: branchName, upstream: upstream ? { - name: upstream.substring(upstream.length - branchName.length), - remote: upstream.substring(0, upstream.length - branchName.length - 1) + name: upstream.substring(index + 1), + remote: upstream.substring(0, index) } : undefined, commit: ref || undefined, ahead: Number(ahead) || 0, diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index d0689a4591c..7a74b77fb33 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabKind } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TextDiffTabInput, NotebookDiffEditorTabInput } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git'; @@ -1272,22 +1272,22 @@ export class Repository implements Disposable { const diffEditorTabsToClose: Tab[] = []; - // Index - const tabs = window.tabGroups.all.map(g => g.tabs).flat(1); - diffEditorTabsToClose.push(...tabs - .filter(t => - t.resource && t.resource.scheme === 'git' && t.kind === TabKind.Diff && - indexResources.some(r => pathEquals(r, t.resource!.fsPath)))); - - // Working Tree - diffEditorTabsToClose.push(...tabs - .filter(t => - t.resource && t.resource.scheme === 'file' && t.kind === TabKind.Diff && - workingTreeResources.some(r => pathEquals(r, t.resource!.fsPath)) && - t.additionalResourcesAndViewIds.find(r => r.resource!.scheme === 'git'))); + for (const tab of window.tabGroups.groups.map(g => g.tabs).flat()) { + const { input } = tab; + if (input instanceof TextDiffTabInput || input instanceof NotebookDiffEditorTabInput) { + if (input.modified.scheme === 'git' && indexResources.some(r => pathEquals(r, input.modified.fsPath))) { + // Index + diffEditorTabsToClose.push(tab); + } + if (input.modified.scheme === 'file' && input.original.scheme === 'git' && workingTreeResources.some(r => pathEquals(r, input.modified.fsPath))) { + // Working Tree + diffEditorTabsToClose.push(tab); + } + } + } // Close editors - diffEditorTabsToClose.forEach(t => t.close()); + window.tabGroups.close(diffEditorTabsToClose, true); } async branch(name: string, _checkout: boolean, _ref?: string): Promise { diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index f6e543257e2..1eaaf4c7493 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; -import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode'; +import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode'; import { Model } from './model'; import { Repository, Resource } from './repository'; import { debounce } from './decorators'; @@ -50,6 +50,20 @@ export class GitTimelineItem extends TimelineItem { return this.shortenRef(this.previousRef); } + setItemDetails(author: string, email: string | undefined, date: string, message: string): void { + this.tooltip = new MarkdownString('', true); + + if (email) { + const emailTitle = localize('git.timeline.email', "Email"); + this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")\n\n`); + } else { + this.tooltip.appendMarkdown(`$(account) **${author}**\n\n`); + } + + this.tooltip.appendMarkdown(`$(history) ${date}\n\n`); + this.tooltip.appendMarkdown(message); + } + private shortenRef(ref: string): string { if (ref === '' || ref === '~' || ref === 'HEAD') { return ref; @@ -155,6 +169,7 @@ export class GitTimelineProvider implements TimelineProvider { const dateType = config.get<'committed' | 'authored'>('date'); const showAuthor = config.get('showAuthor'); + const openComparison = localize('git.timeline.openComparison', "Open Comparison"); const items = commits.map((c, i) => { const date = dateType === 'authored' ? c.authorDate : c.commitDate; @@ -166,12 +181,13 @@ export class GitTimelineProvider implements TimelineProvider { if (showAuthor) { item.description = c.authorName; } - item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format(date)}\n\n${message}`; + + item.setItemDetails(c.authorName!, c.authorEmail, dateFormatter.format(date), message); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { item.command = { - title: 'Open Comparison', + title: openComparison, command: cmd.command, arguments: cmd.arguments, }; @@ -191,12 +207,12 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format(date), Resource.getStatusText(index.type)); + item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(index.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { item.command = { - title: 'Open Comparison', + title: openComparison, command: cmd.command, arguments: cmd.arguments, }; @@ -213,12 +229,12 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format(date), Resource.getStatusText(working.type)); + item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { item.command = { - title: 'Open Comparison', + title: openComparison, command: cmd.command, arguments: cmd.arguments, }; @@ -236,7 +252,7 @@ export class GitTimelineProvider implements TimelineProvider { private ensureProviderRegistration() { if (this.providerDisposable === undefined) { - this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git'], this); + this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git', 'vscode-local-history'], this); } } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 8c832b81988..33ba2d0d916 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -26,20 +26,20 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/which@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" integrity sha1-AW44dim4gXvtZT/jLqtdESecjfY= -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index a3e43f3c020..683a2ab97c4 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -60,12 +60,12 @@ "dependencies": { "node-fetch": "2.6.7", "uuid": "8.1.0", - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-nls": "^5.0.0", - "vscode-tas-client": "^0.1.22" + "vscode-tas-client": "^0.1.31" }, "devDependencies": { - "@types/node": "14.x", + "@types/node": "16.x", "@types/node-fetch": "^2.5.7", "@types/uuid": "8.0.0" }, diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 92313d030a7..439089503ca 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -15,10 +15,11 @@ import { isSupportedEnvironment } from './common/env'; const localize = nls.loadMessageBundle(); const CLIENT_ID = '01ab8ac9400c4e429b23'; - +const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize'; +// TODO: change to stable when that happens +const GITHUB_TOKEN_URL = 'https://insiders.vscode.dev/codeExchangeProxyEndpoints/github/login/oauth/access_token'; +const REDIRECT_URL = 'https://insiders.vscode.dev/redirect'; const NETWORK_ERROR = 'network error'; -const AUTH_RELAY_SERVER = 'vscode-auth.github.com'; -// const AUTH_RELAY_STAGING_SERVER = 'client-auth-staging-14a768b.herokuapp.com'; class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { constructor(private readonly Logger: Log) { @@ -31,14 +32,6 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. } } -function parseQuery(uri: vscode.Uri) { - return uri.query.split('&').reduce((prev: any, current) => { - const queryString = current.split('='); - prev[queryString[0]] = queryString[1]; - return prev; - }, {}); -} - export interface IGitHubServer extends vscode.Disposable { login(scopes: string): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; @@ -115,19 +108,15 @@ async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): P export class GitHubServer implements IGitHubServer { friendlyName = 'GitHub'; type = AuthProviderType.github; - private _statusBarItem: vscode.StatusBarItem | undefined; private _onDidManuallyProvideToken = new vscode.EventEmitter(); - private _pendingStates = new Map(); + private _pendingNonces = new Map(); private _codeExchangePromises = new Map; cancel: vscode.EventEmitter }>(); - private _statusBarCommandId = `${this.type}.provide-manually`; private _disposable: vscode.Disposable; private _uriHandler = new UriEventHandler(this._logger); constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) { - this._disposable = vscode.Disposable.from( - vscode.commands.registerCommand(this._statusBarCommandId, () => this.manuallyProvideUri()), - vscode.window.registerUriHandler(this._uriHandler)); + this._disposable = vscode.window.registerUriHandler(this._uriHandler); } dispose() { @@ -143,7 +132,8 @@ export class GitHubServer implements IGitHubServer { public async login(scopes: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); + const nonce = uuid(); + const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`)); if (!isSupportedEnvironment(callbackUri)) { const token = this._supportDeviceCodeFlow @@ -170,38 +160,46 @@ export class GitHubServer implements IGitHubServer { return token; } - this.updateStatusBarItem(true); + const existingNonces = this._pendingNonces.get(scopes) || []; + this._pendingNonces.set(scopes, [...existingNonces, nonce]); - const state = uuid(); - const existingStates = this._pendingStates.get(scopes) || []; - this._pendingStates.set(scopes, [...existingStates, state]); + const searchParams = new URLSearchParams([ + ['client_id', CLIENT_ID], + ['redirect_uri', REDIRECT_URL], + ['scope', scopes], + ['state', encodeURIComponent(callbackUri.toString(true))] + ]); + const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`); - const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code&authServer=https://github.com`); - await vscode.env.openExternal(uri); + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: localize('signingIn', " $(mark-github) Signing in to github.com..."), + }, async () => { + await vscode.env.openExternal(uri); - // Register a single listener for the URI callback, in case the user starts the login process multiple times - // before completing it. - let codeExchangePromise = this._codeExchangePromises.get(scopes); - if (!codeExchangePromise) { - codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.exchangeCodeForToken(scopes)); - this._codeExchangePromises.set(scopes, codeExchangePromise); - } + // Register a single listener for the URI callback, in case the user starts the login process multiple times + // before completing it. + let codeExchangePromise = this._codeExchangePromises.get(scopes); + if (!codeExchangePromise) { + codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.exchangeCodeForToken(scopes)); + this._codeExchangePromises.set(scopes, codeExchangePromise); + } - return Promise.race([ - codeExchangePromise.promise, - promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { - if (!token) { - reject('Cancelled'); - } else { - resolve(token); - } - }).promise, - new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)) - ]).finally(() => { - this._pendingStates.delete(scopes); - codeExchangePromise?.cancel.fire(); - this._codeExchangePromises.delete(scopes); - this.updateStatusBarItem(false); + return Promise.race([ + codeExchangePromise.promise, + promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { + if (!token) { + reject('Cancelled'); + } else { + resolve(token); + } + }).promise, + new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)) + ]).finally(() => { + this._pendingNonces.delete(scopes); + codeExchangePromise?.cancel.fire(); + this._codeExchangePromises.delete(scopes); + }); }); } @@ -298,29 +296,41 @@ export class GitHubServer implements IGitHubServer { private exchangeCodeForToken: (scopes: string) => PromiseAdapter = (scopes) => async (uri, resolve, reject) => { - const query = parseQuery(uri); - const code = query.code; + const query = new URLSearchParams(uri.query); + const code = query.get('code'); - const acceptedStates = this._pendingStates.get(scopes) || []; - if (!acceptedStates.includes(query.state)) { + const acceptedNonces = this._pendingNonces.get(scopes) || []; + const nonce = query.get('nonce'); + if (!nonce) { + this._logger.error('No nonce in response.'); + return; + } + if (!acceptedNonces.includes(nonce)) { // A common scenario of this happening is if you: // 1. Trigger a sign in with one set of scopes // 2. Before finishing 1, you trigger a sign in with a different set of scopes // In this scenario we should just return and wait for the next UriHandler event // to run as we are probably still waiting on the user to hit 'Continue' - this._logger.info('State not found in accepted state. Skipping this execution...'); + this._logger.info('Nonce not found in accepted nonces. Skipping this execution...'); return; } - const url = `https://${AUTH_RELAY_SERVER}/token?code=${code}&state=${query.state}`; this._logger.info('Exchanging code for token...'); + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}/login/oauth/access_token` : GITHUB_TOKEN_URL; + try { - const result = await fetch(url, { + const body = `code=${code}`; + const result = await fetch(endpointUrl, { method: 'POST', headers: { - Accept: 'application/json' - } + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': body.toString() + + }, + body }); if (result.ok) { @@ -340,48 +350,6 @@ export class GitHubServer implements IGitHubServer { return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`); } - private updateStatusBarItem(isStart?: boolean) { - if (isStart && !this._statusBarItem) { - this._statusBarItem = vscode.window.createStatusBarItem('status.git.signIn', vscode.StatusBarAlignment.Left); - this._statusBarItem.name = localize('status.git.signIn.name', "GitHub Sign-in"); - this._statusBarItem.text = localize('signingIn', "$(mark-github) Signing in to github.com..."); - this._statusBarItem.command = this._statusBarCommandId; - this._statusBarItem.show(); - } - - if (!isStart && this._statusBarItem) { - this._statusBarItem.dispose(); - this._statusBarItem = undefined; - } - } - - private async manuallyProvideUri() { - const uri = await vscode.window.showInputBox({ - prompt: 'Uri', - ignoreFocusOut: true, - validateInput(value) { - if (!value) { - return undefined; - } - const error = localize('validUri', "Please enter a valid Uri from the GitHub login page."); - try { - const uri = vscode.Uri.parse(value.trim()); - if (!uri.scheme || uri.scheme === 'file') { - return error; - } - } catch (e) { - return error; - } - return undefined; - } - }); - if (!uri) { - return; - } - - this._uriHandler.handleUri(vscode.Uri.parse(uri.trim())); - } - public getUserInfo(token: string): Promise<{ id: string; accountName: string }> { return getUserInfo(token, this.getServerUri('/user'), this._logger); } @@ -460,20 +428,9 @@ export class GitHubEnterpriseServer implements IGitHubServer { friendlyName = 'GitHub Enterprise'; type = AuthProviderType.githubEnterprise; - private _onDidManuallyProvideToken = new vscode.EventEmitter(); - private _statusBarCommandId = `github-enterprise.provide-manually`; - private _disposable: vscode.Disposable; + constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) { } - constructor(private readonly _logger: Log, private readonly telemetryReporter: ExperimentationTelemetry) { - this._disposable = vscode.commands.registerCommand(this._statusBarCommandId, async () => { - const token = await vscode.window.showInputBox({ prompt: 'Token', ignoreFocusOut: true }); - this._onDidManuallyProvideToken.fire(token); - }); - } - - dispose() { - this._disposable.dispose(); - } + dispose() { } public async login(scopes: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); diff --git a/extensions/github-authentication/tsconfig.json b/extensions/github-authentication/tsconfig.json index d7aed1836ee..5e4713e9f3b 100644 --- a/extensions/github-authentication/tsconfig.json +++ b/extensions/github-authentication/tsconfig.json @@ -5,6 +5,9 @@ "experimentalDecorators": true, "typeRoots": [ "./node_modules/@types" + ], + "lib": [ + "WebWorker" ] }, "include": [ diff --git a/extensions/github-authentication/yarn.lock b/extensions/github-authentication/yarn.lock index 34c40d62fca..b83ed1357ef 100644 --- a/extensions/github-authentication/yarn.lock +++ b/extensions/github-authentication/yarn.lock @@ -15,32 +15,32 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/uuid@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -axios@^0.21.1: - version "0.21.4" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== +axios@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" + integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.14.7" combined-stream@^1.0.8: version "1.0.8" @@ -54,10 +54,10 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -follow-redirects@^1.14.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +follow-redirects@^1.14.7: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== form-data@^3.0.0: version "3.0.0" @@ -87,12 +87,12 @@ node-fetch@2.6.7: dependencies: whatwg-url "^5.0.0" -tas-client@0.1.21: - version "0.1.21" - resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.21.tgz#62275d5f75266eaae408f7463364748cb92f220d" - integrity sha512-7UuIwOXarCYoCTrQHY5n7M+63XuwMC0sVUdbPQzxqDB9wMjIW0JF39dnp3yoJnxr4jJUVhPtvkkXZbAD0BxCcA== +tas-client@0.1.30: + version "0.1.30" + resolved "https://registry.yarnpkg.com/tas-client/-/tas-client-0.1.30.tgz#f7f03df5d336104db8e8a6eb8269d279eebb0bc7" + integrity sha512-4JINL2r0m7UWU8nIsfWI97SPxjnqz6nk0JtS5ajL5SCksPK8Yjx9yik4DbwwbsbXLJu9C2jasEKnDY40UxEh1g== dependencies: - axios "^0.21.1" + axios "^0.25.0" tr46@~0.0.3: version "0.0.3" @@ -109,12 +109,12 @@ vscode-nls@^5.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-tas-client@^0.1.22: - version "0.1.22" - resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.22.tgz#2dd674b21a94ff4e97db2b6545d9efda8b5f07c3" - integrity sha512-1sYH73nhiSRVQgfZkLQNJW7VzhKM9qNbCe8QyXgiKkLhH4GflDXRPAK4yy4P41jUgula+Fc9G7i5imj1dlKfaw== +vscode-tas-client@^0.1.31: + version "0.1.31" + resolved "https://registry.yarnpkg.com/vscode-tas-client/-/vscode-tas-client-0.1.31.tgz#fec4a36a10214f8af652b4a1f47aed16b22a01df" + integrity sha512-GbvRil0TFdWtG0hvROi/haZkuKNkC/aVjhXWNCKBnS5VwpKtTRnt+o9M5FSETkW7dhfDTK/Jmv53372WtLnVSA== dependencies: - tas-client "0.1.21" + tas-client "0.1.30" webidl-conversions@^3.0.0: version "3.0.1" diff --git a/extensions/github/package.json b/extensions/github/package.json index 7c0013fd7a8..f86f98683af 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -80,7 +80,7 @@ "vscode-nls": "^4.1.2" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock index fabc2469c44..9933736b08e 100644 --- a/extensions/github/yarn.lock +++ b/extensions/github/yarn.lock @@ -99,10 +99,10 @@ dependencies: "@types/node" ">= 8" -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/node@>= 8": version "14.0.23" diff --git a/extensions/grunt/package.json b/extensions/grunt/package.json index 7e5c605ec00..8dd9eced530 100644 --- a/extensions/grunt/package.json +++ b/extensions/grunt/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/grunt/yarn.lock b/extensions/grunt/yarn.lock index f7a30098ef4..22c406bc73f 100644 --- a/extensions/grunt/yarn.lock +++ b/extensions/grunt/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/gulp/package.json b/extensions/gulp/package.json index 995da7fbe5d..8352957083e 100644 --- a/extensions/gulp/package.json +++ b/extensions/gulp/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/gulp/yarn.lock b/extensions/gulp/yarn.lock index f7a30098ef4..22c406bc73f 100644 --- a/extensions/gulp/yarn.lock +++ b/extensions/gulp/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index aefa985c62e..7baceece6fb 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -103,6 +103,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua embeddedLanguages, handledSchemas: ['file'], provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting. + customCapabilities: { rangeFormatting: { editLimit: 10000 } } }, middleware: { // testing the replace / insert mode @@ -229,7 +230,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/; const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/; - languages.registerCompletionItemProvider(documentSelector, { + toDispose.push(languages.registerCompletionItemProvider(documentSelector, { provideCompletionItems(doc, pos) { const results: CompletionItem[] = []; let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos)); @@ -278,7 +279,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } return results; } - }); + })); const promptForLinkedEditingKey = 'html.promptForLinkedEditing'; if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) { diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 5f348e11286..e7244d251c0 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -261,13 +261,13 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-languageclient": "^7.0.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.3" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 0b26ed646a2..4172d2bde23 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^5.1.13", - "vscode-html-languageservice": "^4.2.2", + "vscode-css-languageservice": "^5.2.0", + "vscode-html-languageservice": "^4.2.4", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.3", "vscode-nls": "^5.0.0", @@ -18,7 +18,7 @@ }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "14.x" + "@types/node": "16.x" }, "scripts": { "compile": "npx gulp compile-extension:html-language-features-server", diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index 2144b17ad70..b0df59821b8 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -97,6 +97,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let scopedSettingsSupport = false; let workspaceFoldersSupport = false; let foldingRangeLimit = Number.MAX_VALUE; + let formatterMaxNumberOfEdits = Number.MAX_VALUE; const customDataRequestService: CustomDataRequestService = { getContent(uri: string) { @@ -178,6 +179,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) scopedSettingsSupport = getClientCapability('workspace.configuration', false); workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + formatterMaxNumberOfEdits = params.initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, @@ -426,7 +428,12 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; - return format(languageModes, document, range ?? getFullRange(document), options, settings, enabledModes); + const edits = await format(languageModes, document, range ?? getFullRange(document), options, settings, enabledModes); + if (edits.length > formatterMaxNumberOfEdits) { + const newText = TextDocument.applyEdits(document, edits); + return [TextEdit.replace(getFullRange(document), newText)]; + } + return edits; } return []; } diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 301af0e58c5..36a41173b30 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -360,7 +360,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { const jsDocument = jsDocuments.get(document); const jsLanguageService = await host.getLanguageService(jsDocument); - return getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri); + return [...getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri)]; }, getSemanticTokenLegend(): { types: string[]; modifiers: string[] } { return getSemanticTokenLegend(); diff --git a/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts b/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts index f560559727a..cbcf1b45082 100644 --- a/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts +++ b/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts @@ -16,94 +16,74 @@ export function getSemanticTokenLegend() { return { types: tokenTypes, modifiers: tokenModifiers }; } -export function getSemanticTokens(jsLanguageService: ts.LanguageService, currentTextDocument: TextDocument, fileName: string): SemanticTokenData[] { - //https://ts-ast-viewer.com/#code/AQ0g2CmAuwGbALzAJwG4BQZQGNwEMBnQ4AQQEYBmYAb2C22zgEtJwATJVTRxgcwD27AQAp8AGmAAjAJS0A9POB8+7NQ168oscAJz5wANXwAnLug2bsJmAFcTAO2XAA1MHyvgu-UdOeWbOw8ViAAvpagocBAA +export function* getSemanticTokens(jsLanguageService: ts.LanguageService, document: TextDocument, fileName: string): Iterable { + const { spans } = jsLanguageService.getEncodedSemanticClassifications(fileName, { start: 0, length: document.getText().length }, '2020' as ts.SemanticClassificationFormat); - let resultTokens: SemanticTokenData[] = []; - const collector = (node: ts.Node, typeIdx: number, modifierSet: number) => { - resultTokens.push({ start: currentTextDocument.positionAt(node.getStart()), length: node.getWidth(), typeIdx, modifierSet }); - }; - collectTokens(jsLanguageService, fileName, { start: 0, length: currentTextDocument.getText().length }, collector); + for (let i = 0; i < spans.length;) { + const offset = spans[i++]; + const length = spans[i++]; + const tsClassification = spans[i++]; - return resultTokens; -} - -function collectTokens(jsLanguageService: ts.LanguageService, fileName: string, span: ts.TextSpan, collector: (node: ts.Node, tokenType: number, tokenModifier: number) => void) { - - const program = jsLanguageService.getProgram(); - if (program) { - const typeChecker = program.getTypeChecker(); - - function visit(node: ts.Node) { - if (!node || !ts.textSpanIntersectsWith(span, node.pos, node.getFullWidth())) { - return; - } - if (ts.isIdentifier(node)) { - let symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - if (symbol.flags & ts.SymbolFlags.Alias) { - symbol = typeChecker.getAliasedSymbol(symbol); - } - let typeIdx = classifySymbol(symbol); - if (typeIdx !== undefined) { - let modifierSet = 0; - if (node.parent) { - const parentTypeIdx = tokenFromDeclarationMapping[node.parent.kind]; - if (parentTypeIdx === typeIdx && (node.parent).name === node) { - modifierSet = 1 << TokenModifier.declaration; - } - } - const decl = symbol.valueDeclaration; - const modifiers = decl ? ts.getCombinedModifierFlags(decl) : 0; - const nodeFlags = decl ? ts.getCombinedNodeFlags(decl) : 0; - if (modifiers & ts.ModifierFlags.Static) { - modifierSet |= 1 << TokenModifier.static; - } - if (modifiers & ts.ModifierFlags.Async) { - modifierSet |= 1 << TokenModifier.async; - } - if ((modifiers & ts.ModifierFlags.Readonly) || (nodeFlags & ts.NodeFlags.Const) || (symbol.getFlags() & ts.SymbolFlags.EnumMember)) { - modifierSet |= 1 << TokenModifier.readonly; - } - collector(node, typeIdx, modifierSet); - } - } - } - - ts.forEachChild(node, visit); - } - const sourceFile = program.getSourceFile(fileName); - if (sourceFile) { - visit(sourceFile); + const tokenType = getTokenTypeFromClassification(tsClassification); + if (tokenType === undefined) { + continue; } + + const tokenModifiers = getTokenModifierFromClassification(tsClassification); + const startPos = document.positionAt(offset); + yield { + start: startPos, + length: length, + typeIdx: tokenType, + modifierSet: tokenModifiers + }; } } -function classifySymbol(symbol: ts.Symbol) { - const flags = symbol.getFlags(); - if (flags & ts.SymbolFlags.Class) { - return TokenType.class; - } else if (flags & ts.SymbolFlags.Enum) { - return TokenType.enum; - } else if (flags & ts.SymbolFlags.TypeAlias) { - return TokenType.type; - } else if (flags & ts.SymbolFlags.Type) { - if (flags & ts.SymbolFlags.Interface) { - return TokenType.interface; - } if (flags & ts.SymbolFlags.TypeParameter) { - return TokenType.typeParameter; - } + +// typescript encodes type and modifiers in the classification: +// TSClassification = (TokenType + 1) << 8 + TokenModifier + +const enum TokenType { + class = 0, + enum = 1, + interface = 2, + namespace = 3, + typeParameter = 4, + type = 5, + parameter = 6, + variable = 7, + enumMember = 8, + property = 9, + function = 10, + method = 11, + _ = 12 +} + +const enum TokenModifier { + declaration = 0, + static = 1, + async = 2, + readonly = 3, + defaultLibrary = 4, + local = 5, + _ = 6 +} + +const enum TokenEncodingConsts { + typeOffset = 8, + modifierMask = 255 +} + +function getTokenTypeFromClassification(tsClassification: number): number | undefined { + if (tsClassification > TokenEncodingConsts.modifierMask) { + return (tsClassification >> TokenEncodingConsts.typeOffset) - 1; } - const decl = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; - return decl && tokenFromDeclarationMapping[decl.kind]; + return undefined; } -export const enum TokenType { - class, enum, interface, namespace, typeParameter, type, parameter, variable, property, function, method, _ -} - -export const enum TokenModifier { - declaration, static, async, readonly, _ +function getTokenModifierFromClassification(tsClassification: number) { + return tsClassification & TokenEncodingConsts.modifierMask; } const tokenTypes: string[] = []; @@ -115,6 +95,7 @@ tokenTypes[TokenType.typeParameter] = 'typeParameter'; tokenTypes[TokenType.type] = 'type'; tokenTypes[TokenType.parameter] = 'parameter'; tokenTypes[TokenType.variable] = 'variable'; +tokenTypes[TokenType.enumMember] = 'enumMember'; tokenTypes[TokenType.property] = 'property'; tokenTypes[TokenType.function] = 'function'; tokenTypes[TokenType.method] = 'method'; @@ -124,21 +105,5 @@ tokenModifiers[TokenModifier.async] = 'async'; tokenModifiers[TokenModifier.declaration] = 'declaration'; tokenModifiers[TokenModifier.readonly] = 'readonly'; tokenModifiers[TokenModifier.static] = 'static'; - -const tokenFromDeclarationMapping: { [name: string]: TokenType } = { - [ts.SyntaxKind.VariableDeclaration]: TokenType.variable, - [ts.SyntaxKind.Parameter]: TokenType.parameter, - [ts.SyntaxKind.PropertyDeclaration]: TokenType.property, - [ts.SyntaxKind.ModuleDeclaration]: TokenType.namespace, - [ts.SyntaxKind.EnumDeclaration]: TokenType.enum, - [ts.SyntaxKind.EnumMember]: TokenType.property, - [ts.SyntaxKind.ClassDeclaration]: TokenType.class, - [ts.SyntaxKind.MethodDeclaration]: TokenType.method, - [ts.SyntaxKind.FunctionDeclaration]: TokenType.function, - [ts.SyntaxKind.MethodSignature]: TokenType.method, - [ts.SyntaxKind.GetAccessor]: TokenType.property, - [ts.SyntaxKind.PropertySignature]: TokenType.property, - [ts.SyntaxKind.InterfaceDeclaration]: TokenType.interface, - [ts.SyntaxKind.TypeAliasDeclaration]: TokenType.type, - [ts.SyntaxKind.TypeParameter]: TokenType.typeParameter -}; +tokenModifiers[TokenModifier.local] = 'local'; +tokenModifiers[TokenModifier.defaultLibrary] = 'defaultLibrary'; diff --git a/extensions/html-language-features/server/src/test/semanticTokens.test.ts b/extensions/html-language-features/server/src/test/semanticTokens.test.ts index 2d7677d79a5..2db27dd9f17 100644 --- a/extensions/html-language-features/server/src/test/semanticTokens.test.ts +++ b/extensions/html-language-features/server/src/test/semanticTokens.test.ts @@ -66,8 +66,8 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 6, 1, 'variable.declaration'), t(3, 13, 2, 'variable.declaration'), t(3, 19, 1, 'variable'), - t(5, 15, 1, 'variable.declaration.readonly'), t(5, 20, 2, 'variable'), t(5, 26, 1, 'variable'), t(5, 30, 1, 'variable.readonly'), - t(6, 11, 1, 'variable.declaration'), + t(5, 15, 1, 'variable.declaration.readonly.local'), t(5, 20, 2, 'variable'), t(5, 26, 1, 'variable'), t(5, 30, 1, 'variable.readonly.local'), + t(6, 11, 1, 'variable.declaration.local'), t(7, 10, 2, 'variable') ]); }); @@ -87,8 +87,8 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 11, 3, 'function.declaration'), t(3, 15, 2, 'parameter.declaration'), - t(4, 11, 3, 'function'), t(4, 15, 4, 'interface'), t(4, 20, 3, 'method'), t(4, 24, 2, 'parameter'), - t(6, 6, 6, 'variable'), t(6, 13, 8, 'property'), t(6, 24, 5, 'method'), t(6, 35, 7, 'method'), t(6, 43, 1, 'parameter.declaration'), t(6, 48, 3, 'function'), t(6, 52, 1, 'parameter') + t(4, 11, 3, 'function'), t(4, 15, 4, 'variable.defaultLibrary'), t(4, 20, 3, 'method.defaultLibrary'), t(4, 24, 2, 'parameter'), + t(6, 6, 6, 'variable.defaultLibrary'), t(6, 13, 8, 'property.defaultLibrary'), t(6, 24, 5, 'method.defaultLibrary'), t(6, 35, 7, 'method.defaultLibrary'), t(6, 43, 1, 'parameter.declaration'), t(6, 48, 3, 'function'), t(6, 52, 1, 'parameter') ]); }); @@ -135,8 +135,8 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 12, 8, 'interface.declaration'), t(3, 23, 1, 'property.declaration'), t(3, 34, 1, 'property.declaration'), - t(4, 8, 1, 'variable.declaration.readonly'), t(4, 30, 8, 'interface'), - t(5, 8, 3, 'variable.declaration.readonly'), t(5, 15, 1, 'parameter.declaration'), t(5, 18, 8, 'interface'), t(5, 31, 1, 'parameter'), t(5, 33, 1, 'property'), t(5, 37, 1, 'parameter'), t(5, 39, 1, 'property') + t(4, 8, 1, 'variable.declaration.readonly'), t(4, 14, 1, 'property.declaration'), t(4, 20, 1, 'property.declaration'), t(4, 30, 8, 'interface'), + t(5, 8, 3, 'function.declaration.readonly'), t(5, 15, 1, 'parameter.declaration'), t(5, 18, 8, 'interface'), t(5, 31, 1, 'parameter'), t(5, 33, 1, 'property'), t(5, 37, 1, 'parameter'), t(5, 39, 1, 'property') ]); }); @@ -155,9 +155,9 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 8, 1, 'variable.declaration.readonly'), - t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.static.readonly'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface'), - t(5, 13, 1, 'enum.declaration'), t(5, 17, 1, 'property.declaration.readonly'), t(5, 24, 1, 'property.declaration.readonly'), t(5, 28, 1, 'property.readonly'), - t(6, 2, 7, 'variable'), t(6, 10, 3, 'method'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.static.readonly'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly'), + t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.static.readonly'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface.defaultLibrary'), + t(5, 13, 1, 'enum.declaration'), t(5, 17, 1, 'enumMember.declaration.readonly'), t(5, 24, 1, 'enumMember.declaration.readonly'), t(5, 28, 1, 'enumMember.readonly'), + t(6, 2, 7, 'variable.defaultLibrary'), t(6, 10, 3, 'method.defaultLibrary'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.static.readonly'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly.defaultLibrary'), ]); }); @@ -176,9 +176,9 @@ suite('HTML Semantic Tokens', () => { /*9*/'', ]; await assertTokens(input, [ - t(3, 7, 5, 'type.declaration'), t(3, 15, 3, 'interface') /* to investiagte */, + t(3, 7, 5, 'type.declaration'), t(3, 15, 3, 'interface.defaultLibrary') /* to investiagte */, t(4, 11, 1, 'function.declaration'), t(4, 13, 1, 'typeParameter.declaration'), t(4, 23, 5, 'type'), t(4, 30, 1, 'parameter.declaration'), t(4, 33, 1, 'typeParameter'), t(4, 47, 1, 'typeParameter'), - t(5, 12, 1, 'typeParameter'), t(5, 29, 3, 'interface'), t(5, 41, 5, 'type'), + t(5, 12, 1, 'typeParameter'), t(5, 29, 3, 'class.defaultLibrary'), t(5, 41, 5, 'type'), ]); }); @@ -197,7 +197,7 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 11, 1, 'function.declaration'), t(3, 13, 1, 'typeParameter.declaration'), t(3, 16, 2, 'parameter.declaration'), t(3, 20, 1, 'typeParameter'), t(3, 24, 1, 'typeParameter'), t(3, 39, 2, 'parameter'), - t(6, 2, 6, 'variable'), t(6, 9, 5, 'method') + t(6, 2, 6, 'variable.defaultLibrary'), t(6, 9, 5, 'method.defaultLibrary') ]); }); @@ -215,11 +215,11 @@ suite('HTML Semantic Tokens', () => { /*9*/'', ]; await assertTokens(input, [ - t(3, 2, 6, 'variable'), t(3, 9, 5, 'method') + t(3, 2, 6, 'variable.defaultLibrary'), t(3, 9, 5, 'method.defaultLibrary') ], [Range.create(Position.create(2, 0), Position.create(4, 0))]); await assertTokens(input, [ - t(6, 2, 6, 'variable'), + t(6, 2, 6, 'variable.defaultLibrary'), ], [Range.create(Position.create(6, 2), Position.create(6, 8))]); }); diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index 5ef1243cf83..729486ef482 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -7,27 +7,27 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -vscode-css-languageservice@^5.1.13: - version "5.1.13" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.1.13.tgz#debc7c8368223b211a734cb7eb7789c586d3e2d9" - integrity sha512-FA0foqMzMmEoO0WJP+MjoD4dRERhKS+Ag+yBrtmWQDmw2OuZ1R/5FkvI/XdTkCpHmTD9VMczugpHRejQyTXCNQ== +vscode-css-languageservice@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.2.0.tgz#84fa95f8314c742080c09f623bf9f0727621eebf" + integrity sha512-FR5yDEfzbXJtYmZYrA7JWFcRSLHsJw3nv55XAmx7qdwRpFj9yy0ulKfN/NUUdiZW2jZU2fD/+Y4VJYPdafHDag== dependencies: - vscode-languageserver-textdocument "^1.0.1" + vscode-languageserver-textdocument "^1.0.4" vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" - vscode-uri "^3.0.2" + vscode-uri "^3.0.3" -vscode-html-languageservice@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.2.2.tgz#e580b8f22b1b8c1dc0d6aaeda5a861f8b4120e4e" - integrity sha512-4ICwlpplGbiNQq6D/LZr4qLbPZuMmnSQeX/57UAYP7jD1LOvKeru4lVI+f6d6Eyd7uS46nLJ5DUY4AAlq35C0g== +vscode-html-languageservice@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.2.4.tgz#a99a877409a4c745c61ce1736ae5d8609e10e743" + integrity sha512-1HqvXKOq9WlZyW4HTD+0XzrjZoZ/YFrgQY2PZqktbRloHXVAUKm6+cAcvZi4YqKPVn05/CK7do+KBHfuSaEdbg== dependencies: - vscode-languageserver-textdocument "^1.0.3" + vscode-languageserver-textdocument "^1.0.4" vscode-languageserver-types "^3.16.0" vscode-nls "^5.0.0" vscode-uri "^3.0.3" @@ -45,16 +45,16 @@ vscode-languageserver-protocol@3.16.0: vscode-jsonrpc "6.0.0" vscode-languageserver-types "3.16.0" -vscode-languageserver-textdocument@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f" - integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA== - vscode-languageserver-textdocument@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.3.tgz#879f2649bfa5a6e07bc8b392c23ede2dfbf43eff" integrity sha512-ynEGytvgTb6HVSUwPJIAZgiHQmPCx8bZ8w5um5Lz+q5DjP0Zj8wTFhQpyg8xaMvefDytw2+HH5yzqS+FhsR28A== +vscode-languageserver-textdocument@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157" + integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ== + vscode-languageserver-types@3.16.0, vscode-languageserver-types@^3.16.0: version "3.16.0" resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" @@ -72,11 +72,6 @@ vscode-nls@^5.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== -vscode-uri@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.2.tgz#ecfd1d066cb8ef4c3a208decdbab9a8c23d055d0" - integrity sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA== - vscode-uri@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index 5a834689bfa..d77421b701c 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== balanced-match@^1.0.0: version "1.0.0" diff --git a/extensions/html/build/update-grammar.js b/extensions/html/build/update-grammar.mjs similarity index 86% rename from extensions/html/build/update-grammar.js rename to extensions/html/build/update-grammar.mjs index 03d69042f5a..1fd330e0289 100644 --- a/extensions/html/build/update-grammar.js +++ b/extensions/html/build/update-grammar.mjs @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ // @ts-check -'use strict'; -var updateGrammar = require('vscode-grammar-updater'); +import * as vscodeGrammarUpdater from 'vscode-grammar-updater'; function patchGrammar(grammar) { let patchCount = 0; @@ -39,6 +38,6 @@ function patchGrammar(grammar) { const tsGrammarRepo = 'textmate/html.tmbundle'; const grammarPath = 'Syntaxes/HTML.plist'; -updateGrammar.update(tsGrammarRepo, grammarPath, './syntaxes/html.tmLanguage.json', grammar => patchGrammar(grammar)); +vscodeGrammarUpdater.update(tsGrammarRepo, grammarPath, './syntaxes/html.tmLanguage.json', grammar => patchGrammar(grammar)); diff --git a/extensions/html/package.json b/extensions/html/package.json index 4a8e8911d3c..aad035c05cc 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -9,7 +9,7 @@ "vscode": "0.10.x" }, "scripts": { - "update-grammar": "node ./build/update-grammar.js" + "update-grammar": "node ./build/update-grammar.mjs" }, "contributes": { "languages": [ diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index 286cc9b4daa..57bbb338210 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -79,7 +79,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-nls": "^5.0.0" }, "repository": { diff --git a/extensions/image-preview/yarn.lock b/extensions/image-preview/yarn.lock index 89c03026025..2e316e14a9f 100644 --- a/extensions/image-preview/yarn.lock +++ b/extensions/image-preview/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 1efd245ea17..51ec71f86c5 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -31,7 +31,9 @@ "commands": [ { "command": "ipynb.newUntitledIpynb", - "title": "Jupyter Notebook" + "title": "New Jupyter Notebook", + "shortTitle": "Jupyter Notebook", + "category": "Create" }, { "command": "ipynb.openIpynbInNotebookEditor", @@ -53,15 +55,13 @@ "menus": { "file/newFile": [ { - "command": "ipynb.newUntitledIpynb", - "when": "!jupyterEnabled" + "command": "ipynb.newUntitledIpynb" } ], "commandPalette": [ - { - "command": "ipynb.newUntitledIpynb", - "when": "false" - }, + { + "command": "ipynb.newUntitledIpynb" + }, { "command": "ipynb.openIpynbInNotebookEditor", "when": "false" @@ -79,7 +79,7 @@ "uuid": "^8.3.2" }, "devDependencies": { - "@jupyterlab/coreutils": "^3.1.0", + "@jupyterlab/nbformat": "^3.2.9", "@types/uuid": "^8.3.1" }, "repository": { diff --git a/extensions/ipynb/src/cellIdService.ts b/extensions/ipynb/src/cellIdService.ts index 76d699a977e..afcad72e9e7 100644 --- a/extensions/ipynb/src/cellIdService.ts +++ b/extensions/ipynb/src/cellIdService.ts @@ -8,7 +8,7 @@ import { v4 as uuid } from 'uuid'; import { getCellMetadata } from './serializers'; import { CellMetadata } from './common'; import { getNotebookMetadata } from './notebookSerializer'; -import { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; /** * Ensure all new cells in notebooks with nbformat >= 4.5 have an id. diff --git a/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts index d361d554877..de72fbbbfc3 100644 --- a/extensions/ipynb/src/common.ts +++ b/extensions/ipynb/src/common.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; /** * Metadata we store in VS Code cell output items. diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts index 3152bbfa8be..50a3a1271b0 100644 --- a/extensions/ipynb/src/deserializers.ts +++ b/extensions/ipynb/src/deserializers.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; import { extensions, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode'; import { CellMetadata, CellOutputMetadata } from './common'; @@ -308,7 +308,9 @@ function createNotebookCellDataFromCodeCell(cell: nbformat.ICodeCell, cellLangua ? { executionOrder: cell.execution_count as number } : {}; - const cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguage); + const vscodeCustomMetadata = cell.metadata['vscode'] as { [key: string]: any } | undefined; + const cellLanguageId = vscodeCustomMetadata && vscodeCustomMetadata.languageId && typeof vscodeCustomMetadata.languageId === 'string' ? vscodeCustomMetadata.languageId : cellLanguage; + const cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguageId); cellData.outputs = outputs; cellData.metadata = { custom: getNotebookCellMetadata(cell) }; diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index 5596117faab..c2c7044bd77 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -39,7 +39,11 @@ export function activate(context: vscode.ExtensionContext) { vscode.languages.registerCodeLensProvider({ pattern: '**/*.ipynb' }, { provideCodeLenses: (document) => { - if (document.uri.scheme === 'vscode-notebook-cell') { + if ( + document.uri.scheme === 'vscode-notebook-cell' || + document.uri.scheme === 'vscode-notebook-cell-metadata' || + document.uri.scheme === 'vscode-notebook-cell-output' + ) { return []; } const codelens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), { title: 'Open in Notebook Editor', command: 'ipynb.openIpynbInNotebookEditor', arguments: [document.uri] }); diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts index 7d5cef2b3d3..37eee49b1be 100644 --- a/extensions/ipynb/src/notebookSerializer.ts +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; import * as detectIndent from 'detect-indent'; import * as vscode from 'vscode'; import { defaultNotebookFormat } from './constants'; @@ -83,9 +83,11 @@ export class NotebookSerializer implements vscode.NotebookSerializer { public serializeNotebookToString(data: vscode.NotebookData): string { const notebookContent = getNotebookMetadata(data); + // use the preferred language from document metadata or the first cell language as the notebook preferred cell language + const preferredCellLanguage = notebookContent.metadata?.language_info?.name ?? data.cells[0].languageId; notebookContent.cells = data.cells - .map(cell => createJupyterCellFromNotebookCell(cell)) + .map(cell => createJupyterCellFromNotebookCell(cell, preferredCellLanguage)) .map(pruneCell); const indentAmount = data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string' ? diff --git a/extensions/ipynb/src/serializers.ts b/extensions/ipynb/src/serializers.ts index f0d8aa124a1..078bb408cc5 100644 --- a/extensions/ipynb/src/serializers.ts +++ b/extensions/ipynb/src/serializers.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode'; import { CellMetadata, CellOutputMetadata } from './common'; import { textMimeTypes } from './deserializers'; @@ -17,7 +17,8 @@ enum CellOutputMimeTypes { } export function createJupyterCellFromNotebookCell( - vscCell: NotebookCellData + vscCell: NotebookCellData, + preferredLanguage: string | undefined ): nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell { let cell: nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell; if (vscCell.kind === NotebookCellKind.Markup) { @@ -25,7 +26,7 @@ export function createJupyterCellFromNotebookCell( } else if (vscCell.languageId === 'raw') { cell = createRawCellFromNotebookCell(vscCell); } else { - cell = createCodeCellFromNotebookCell(vscCell); + cell = createCodeCellFromNotebookCell(vscCell, preferredLanguage); } return cell; } @@ -56,14 +57,27 @@ export function sortObjectPropertiesRecursively(obj: any): any { export function getCellMetadata(cell: NotebookCell | NotebookCellData) { return cell.metadata?.custom as CellMetadata | undefined; } -function createCodeCellFromNotebookCell(cell: NotebookCellData): nbformat.ICodeCell { +function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguage: string | undefined): nbformat.ICodeCell { const cellMetadata = getCellMetadata(cell); + let metadata = cellMetadata?.metadata || {}; // This cannot be empty. + if (cell.languageId !== preferredLanguage) { + metadata = { + ...metadata, + vscode: { + languageId: cell.languageId + } + }; + } else { + // cell current language is the same as the preferred cell language in the document, flush the vscode custom language id metadata + metadata.vscode = undefined; + } + const codeCell: any = { cell_type: 'code', execution_count: cell.executionSummary?.executionOrder ?? null, source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), outputs: (cell.outputs || []).map(translateCellDisplayOutput), - metadata: cellMetadata?.metadata || {} // This cannot be empty. + metadata: metadata }; if (cellMetadata?.id) { codeCell.id = cellMetadata.id; diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts index 26ca5b32aa9..d945d1f4223 100644 --- a/extensions/ipynb/src/test/serializers.test.ts +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { nbformat } from '@jupyterlab/coreutils'; +import * as nbformat from '@jupyterlab/nbformat'; import * as assert from 'assert'; import * as vscode from 'vscode'; import { jupyterCellOutputToCellOutput, jupyterNotebookModelToNotebookData } from '../deserializers'; diff --git a/extensions/ipynb/yarn.lock b/extensions/ipynb/yarn.lock index 40543cbce70..c932570ecc0 100644 --- a/extensions/ipynb/yarn.lock +++ b/extensions/ipynb/yarn.lock @@ -7,162 +7,28 @@ resolved "https://registry.yarnpkg.com/@enonic/fnv-plus/-/fnv-plus-1.3.0.tgz#be65a7b128a3b544f60aea3ef978d938e85869f3" integrity sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw== -"@jupyterlab/coreutils@^3.1.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz#dd4d887bdedfea4c8545d46d297531749cb13724" - integrity sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ== +"@jupyterlab/nbformat@^3.2.9": + version "3.2.9" + resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.2.9.tgz#e7d854719612133498af4280d9a8caa0873205b0" + integrity sha512-WSf9OQo8yfFjyodbXRdFoaNwMkaAL5jFZiD6V2f8HqI380ipansWrrV7R9CGzPfgKHpUGZMO1tYKmUwzMhvZ4w== dependencies: - "@phosphor/commands" "^1.7.0" - "@phosphor/coreutils" "^1.3.1" - "@phosphor/disposable" "^1.3.0" - "@phosphor/properties" "^1.1.3" - "@phosphor/signaling" "^1.3.0" - ajv "^6.5.5" - json5 "^2.1.0" - minimist "~1.2.0" - moment "^2.24.0" - path-posix "~1.0.0" - url-parse "~1.4.3" + "@lumino/coreutils" "^1.5.3" -"@phosphor/algorithm@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@phosphor/algorithm/-/algorithm-1.2.0.tgz#4a19aa59261b7270be696672dc3f0663f7bef152" - integrity sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA== - -"@phosphor/commands@^1.7.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@phosphor/commands/-/commands-1.7.2.tgz#df724f2896ae43c4a3a9e2b5a6445a15e0d60487" - integrity sha512-iSyBIWMHsus323BVEARBhuVZNnVel8USo+FIPaAxGcq+icTSSe6+NtSxVQSmZblGN6Qm4iw6I6VtiSx0e6YDgQ== - dependencies: - "@phosphor/algorithm" "^1.2.0" - "@phosphor/coreutils" "^1.3.1" - "@phosphor/disposable" "^1.3.1" - "@phosphor/domutils" "^1.1.4" - "@phosphor/keyboard" "^1.1.3" - "@phosphor/signaling" "^1.3.1" - -"@phosphor/coreutils@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@phosphor/coreutils/-/coreutils-1.3.1.tgz#441e34f42340f7faa742a88b2a181947a88d7226" - integrity sha512-9OHCn8LYRcPU/sbHm5v7viCA16Uev3gbdkwqoQqlV+EiauDHl70jmeL7XVDXdigl66Dz0LI11C99XOxp+s3zOA== - -"@phosphor/disposable@^1.3.0", "@phosphor/disposable@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@phosphor/disposable/-/disposable-1.3.1.tgz#be98fe12bd8c9a4600741cb83b0a305df28628f3" - integrity sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw== - dependencies: - "@phosphor/algorithm" "^1.2.0" - "@phosphor/signaling" "^1.3.1" - -"@phosphor/domutils@^1.1.4": - version "1.1.4" - resolved "https://registry.yarnpkg.com/@phosphor/domutils/-/domutils-1.1.4.tgz#4c6aecf7902d3793b45db325319340e0a0b5543b" - integrity sha512-ivwq5TWjQpKcHKXO8PrMl+/cKqbgxPClPiCKc1gwbMd+6hnW5VLwNG0WBzJTxCzXK43HxX18oH+tOZ3E04wc3w== - -"@phosphor/keyboard@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@phosphor/keyboard/-/keyboard-1.1.3.tgz#e5fd13af0479034ef0b5fffcf43ef2d4a266b5b6" - integrity sha512-dzxC/PyHiD6mXaESRy6PZTd9JeK+diwG1pyngkyUf127IXOEzubTIbu52VSdpGBklszu33ws05BAGDa4oBE4mQ== - -"@phosphor/properties@^1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@phosphor/properties/-/properties-1.1.3.tgz#63e4355be5e22a411c566fd1860207038f171598" - integrity sha512-GiglqzU77s6+tFVt6zPq9uuyu/PLQPFcqZt914ZhJ4cN/7yNI/SLyMzpYZ56IRMXvzK9TUgbRna6URE3XAwFUg== - -"@phosphor/signaling@^1.3.0", "@phosphor/signaling@^1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@phosphor/signaling/-/signaling-1.3.1.tgz#1cd10b069bdb2c9adb3ba74245b30141e5afc2d7" - integrity sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg== - dependencies: - "@phosphor/algorithm" "^1.2.0" +"@lumino/coreutils@^1.5.3": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-1.12.0.tgz#fbdef760f736eaf2bd396a5c6fc3a68a4b449b15" + integrity sha512-DSglh4ylmLi820CNx9soJmDJCpUgymckdWeGWuN0Ash5g60oQvrQDfosVxEhzmNvtvXv45WZEqSBzDP6E5SEmQ== "@types/uuid@^8.3.1": version "8.3.1" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== -ajv@^6.5.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json5@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - -minimist@^1.2.5, minimist@~1.2.0: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -moment@^2.24.0: - version "2.29.1" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - -path-posix@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" - integrity sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-parse@~1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" diff --git a/extensions/jake/package.json b/extensions/jake/package.json index 4bb8793f3ee..0e5ebc4ea8b 100644 --- a/extensions/jake/package.json +++ b/extensions/jake/package.json @@ -20,7 +20,7 @@ "vscode-nls": "^4.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "main": "./out/main", "activationEvents": [ diff --git a/extensions/jake/yarn.lock b/extensions/jake/yarn.lock index f7a30098ef4..22c406bc73f 100644 --- a/extensions/jake/yarn.lock +++ b/extensions/jake/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^4.0.0: version "4.0.0" diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 5eab3c69bd1..405006f6e29 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -3566,59 +3566,10 @@ "name": "variable.language.arguments.js", "match": "(?$" }, "afterText": { - "pattern": "/^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>$", + "pattern": "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>$", "flags": "i" }, "action": { diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 8fa87867bb4..7fd2b32ca2f 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -124,13 +124,13 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua let isClientReady = false; - commands.registerCommand('json.clearCache', async () => { + toDispose.push(commands.registerCommand('json.clearCache', async () => { if (isClientReady && runtime.schemaRequests.clearCache) { const cachedSchemas = await runtime.schemaRequests.clearCache(); await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas); } window.showInformationMessage(localize('json.clearCache.completed', "JSON schema cache cleared.")); - }); + })); // Options to control the language client const clientOptions: LanguageClientOptions = { @@ -306,9 +306,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); - extensions.onDidChange(_ => { + toDispose.push(extensions.onDidChange(_ => { client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); - }); + })); // manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652. updateFormatterRegistration(); @@ -327,7 +327,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua client.onNotification(ResultLimitReachedNotification.type, async message => { const shouldPrompt = context.globalState.get(StorageIds.maxItemsExceededInformation) !== false; if (shouldPrompt) { - const ok = localize('ok', "Ok"); + const ok = localize('ok', "OK"); const openSettings = localize('goToSetting', 'Open Settings'); const neverAgain = localize('yes never again', "Don't Show Again"); const pick = await window.showInformationMessage(`${message}\n${localize('configureLimit', 'Use setting \'{0}\' to configure the limit.', SettingIds.maxItemsComputed)}`, ok, openSettings, neverAgain); diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 983230c2928..d82ebaa85f5 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -142,12 +142,12 @@ }, "dependencies": { "request-light": "^0.5.7", - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-languageclient": "^7.0.0", "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index ab5a28f62b7..cb3fd6c2c78 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,13 +14,13 @@ "dependencies": { "jsonc-parser": "^3.0.0", "request-light": "^0.5.7", - "vscode-json-languageservice": "^4.2.0", + "vscode-json-languageservice": "^4.2.1", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "14.x" + "@types/node": "16.x" }, "scripts": { "prepublishOnly": "npm run clean && npm run compile", diff --git a/extensions/json-language-features/server/src/node/jsonServerMain.ts b/extensions/json-language-features/server/src/node/jsonServerMain.ts index 4cb387095aa..ad1ae439e59 100644 --- a/extensions/json-language-features/server/src/node/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/node/jsonServerMain.ts @@ -36,7 +36,7 @@ function getHTTPRequestService(): RequestService { function getFileRequestService(): RequestService { return { - getContent(location: string, encoding?: string) { + getContent(location: string, encoding?: BufferEncoding) { return new Promise((c, e) => { const uri = Uri.parse(location); fs.readFile(uri.fsPath, encoding, (err, buf) => { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index ae20eed06c0..483e6cc060a 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== jsonc-parser@^3.0.0: version "3.0.0" @@ -22,10 +22,10 @@ request-light@^0.5.7: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.7.tgz#1c448c22153b55d2cd278eb414df24a5ad6e6d5e" integrity sha512-i/wKzvcx7Er8tZnvqSxWuNO5ZGggu2UgZAqj/RyZ0si7lBTXL7kZiI/dWxzxnQjaY7s5HEy1qK21Do4Ncr6cVw== -vscode-json-languageservice@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.2.0.tgz#df0693b69ba2fbf0a6add896087b6f1c9c38f06a" - integrity sha512-XNawv0Vdy/sUK0S+hGf7cq/qsVAbIniGJr89TvZOqMCNJmpgKTy1e8PL1aWW0uy6BfWMG7vxa5lZb3ypuFtuGQ== +vscode-json-languageservice@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz#94b6f471ece193bf4a1ef37f6ab5cce86d50a8b4" + integrity sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA== dependencies: jsonc-parser "^3.0.0" vscode-languageserver-textdocument "^1.0.3" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 9f393dd5453..2049e0f32bf 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -2,15 +2,15 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== balanced-match@^1.0.0: version "1.0.0" diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 9640a863021..c6cc3e5c1f7 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "0805e96e0856cab9b33a1f1025f5613cfa19189b" + "commitHash": "7b7801f41ce4ac1303bd17e057dbe677e24f597f" } }, "license": "MIT", diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index 3d78b0f5975..bd3acb8d6bb 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/0805e96e0856cab9b33a1f1025f5613cfa19189b", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/7b7801f41ce4ac1303bd17e057dbe677e24f597f", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", @@ -280,6 +280,10 @@ "match": "\\b(? process.exit(1)); diff --git a/extensions/markdown-language-features/esbuild-preview.js b/extensions/markdown-language-features/esbuild-preview.js new file mode 100644 index 00000000000..727353c6e44 --- /dev/null +++ b/extensions/markdown-language-features/esbuild-preview.js @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path = require('path'); +const esbuild = require('esbuild'); + +const args = process.argv.slice(2); + +const isWatch = args.indexOf('--watch') >= 0; + +let outputRoot = __dirname; +const outputRootIndex = args.indexOf('--outputRoot'); +if (outputRootIndex >= 0) { + outputRoot = args[outputRootIndex + 1]; +} + +const outDir = path.join(outputRoot, 'media'); + +esbuild.build({ + entryPoints: [ + path.join(__dirname, 'preview-src', 'index.ts'), + path.join(__dirname, 'preview-src', 'pre'), + ], + bundle: true, + minify: true, + sourcemap: false, + format: 'esm', + outdir: outDir, + platform: 'browser', + target: ['es2020'], + watch: isWatch, + incremental: isWatch, +}).catch(() => process.exit(1)); diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index e46dfda8888..99f788be65c 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -15,6 +15,9 @@ "categories": [ "Programming Languages" ], + "enabledApiProposals": [ + "textEditorDrop" + ], "activationEvents": [ "onLanguage:markdown", "onCommand:markdown.preview.toggleLock", @@ -424,19 +427,20 @@ "watch": "npm run build-preview && gulp watch-extension:markdown-language-features", "vscode:prepublish": "npm run build-ext && npm run build-preview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", - "build-preview": "npx webpack-cli --mode production", - "build-notebook": "node ./esbuild", + "build-notebook": "node ./esbuild-notebook", + "build-preview": "node ./esbuild-preview", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { + "@vscode/extension-telemetry": "0.4.10", "dompurify": "^2.3.3", - "highlight.js": "^10.4.1", + "highlight.js": "^11.4.0", "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.1", "morphdom": "^2.6.1", - "@vscode/extension-telemetry": "0.4.6", - "vscode-nls": "^5.0.0" + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" }, "devDependencies": { "@types/dompurify": "^2.3.1", diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 7602cee8ed8..022ab92f11e 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import * as vscode from 'vscode'; +import * as URI from 'vscode-uri'; import { CommandManager } from './commandManager'; import * as commands from './commands/index'; import LinkProvider from './features/documentLinkProvider'; @@ -45,6 +47,43 @@ export function activate(context: vscode.ExtensionContext) { logger.updateConfiguration(); previewManager.updateConfiguration(); })); + + context.subscriptions.push(vscode.workspace.onWillDropOnTextEditor(e => { + e.waitUntil((async () => { + const resourceUrls = await e.dataTransfer.get('resourceurls')?.asString(); + if (!resourceUrls) { + return; + } + + const uris: vscode.Uri[] = []; + for (const resource of JSON.parse(resourceUrls)) { + try { + uris.push(vscode.Uri.parse(resource)); + } catch { + // noop + } + } + + if (!uris.length) { + return; + } + + const snippet = new vscode.SnippetString(); + uris.forEach((uri, i) => { + const rel = path.relative(URI.Utils.dirname(e.editor.document.uri).fsPath, uri.fsPath); + + snippet.appendText('['); + snippet.appendTabstop(); + snippet.appendText(`](${rel})`); + + if (i <= uris.length - 1 && uris.length > 1) { + snippet.appendText(' '); + } + }); + + return e.editor.insertSnippet(snippet, e.position); + })()); + })); } function registerMarkdownLanguageFeatures( diff --git a/extensions/markdown-language-features/src/features/documentLinkProvider.ts b/extensions/markdown-language-features/src/features/documentLinkProvider.ts index 86be23fdfb9..9c028de7d90 100644 --- a/extensions/markdown-language-features/src/features/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/features/documentLinkProvider.ts @@ -5,10 +5,10 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as uri from 'vscode-uri'; import { OpenDocumentLinkCommand } from '../commands/openDocumentLink'; import { MarkdownEngine } from '../markdownEngine'; import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/links'; -import { dirname } from '../util/path'; const localize = nls.loadMessageBundle(); @@ -46,7 +46,7 @@ function parseLink( resourceUri = vscode.Uri.joinPath(root, tempUri.path); } } else { - const base = document.uri.with({ path: dirname(document.uri.fsPath) }); + const base = uri.Utils.dirname(document.uri); resourceUri = vscode.Uri.joinPath(base, tempUri.path); } } @@ -104,7 +104,7 @@ export function stripAngleBrackets(link: string) { } const linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g; -const referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g; +const referenceLinkPattern = /((?<=^|[^\]])\[((?:\\\]|[^\]])+)\])(?!:)(?:[^\[]|$|\[\s*?([^\s\]]*?)\])/g; const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm; const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm; @@ -186,10 +186,10 @@ export default class LinkProvider implements vscode.DocumentLinkProvider { let reference = match[3]; if (reference) { // [text][ref] const pre = match[1]; - const offset = (match.index || 0) + pre.length; + const offset = (match.index || 0) + pre.length + 1; linkStart = document.positionAt(offset); linkEnd = document.positionAt(offset + reference.length); - } else if (match[2]) { // [ref][] + } else if (match[2]) { // [ref][], [ref] reference = match[2]; const offset = (match.index || 0) + 1; linkStart = document.positionAt(offset); diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index dbe6890912f..3b3f1fe6c8c 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -5,13 +5,13 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as uri from 'vscode-uri'; import { Logger } from '../logger'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; import { openDocumentLink, resolveDocumentLink, resolveUriToMarkdownFile } from '../util/openDocumentLink'; -import * as path from '../util/path'; import { WebviewResourceProvider } from '../util/resources'; import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { urlToUri } from '../util/url'; @@ -179,31 +179,6 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } })); - this._register(this._webviewPanel.onDidChangeViewState(async () => { - if (this._disposed) { - return; - } - - if (this._webviewPanel.active) { - let document: vscode.TextDocument; - try { - document = await vscode.workspace.openTextDocument(this._resource); - } catch { - return; - } - - if (this._disposed) { - return; - } - - const content = await this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state); - if (!this._webviewPanel.active && !this._disposed) { - // Update the html so we can show it properly when restoring it - this._webviewPanel.webview.html = content.html; - } - } - })); - this._register(this._webviewPanel.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { if (e.source !== this._resource.toString()) { return; @@ -343,7 +318,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { return; } - const shouldReloadPage = forceUpdate || !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString(); + const shouldReloadPage = forceUpdate || !this.currentVersion || this.currentVersion.resource.toString() !== pendingVersion.resource.toString() || !this._webviewPanel.visible; this.currentVersion = pendingVersion; const content = await (shouldReloadPage @@ -425,7 +400,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { const srcs = new Set(containingImages.map(img => img.src)); // Delete stale file watchers. - for (const [src, watcher] of [...this._fileWatchersBySrc]) { + for (const [src, watcher] of this._fileWatchersBySrc) { if (!srcs.has(src)) { watcher.dispose(); this._fileWatchersBySrc.delete(src); @@ -464,7 +439,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { baseRoots.push(...workspaceRoots); } } else { - baseRoots.push(this._resource.with({ path: path.dirname(this._resource.path) })); + baseRoots.push(uri.Utils.dirname(this._resource)); } return baseRoots; @@ -792,9 +767,10 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow } private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string { + const resourceLabel = uri.Utils.basename(resource); return locked - ? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath)) - : localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath)); + ? localize('lockedPreviewTitle', '[Preview] {0}', resourceLabel) + : localize('previewTitle', 'Preview {0}', resourceLabel); } public get position(): vscode.ViewColumn | undefined { diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 172d37c838a..5c4a16072e2 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -5,11 +5,11 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as uri from 'vscode-uri'; import { Logger } from '../logger'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security'; -import { basename, dirname, isAbsolute, join } from '../util/path'; import { WebviewResourceProvider } from '../util/resources'; import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig'; @@ -128,7 +128,7 @@ export class MarkdownContentProvider { public provideFileNotFoundContent( resource: vscode.Uri, ): string { - const resourcePath = basename(resource.fsPath); + const resourcePath = uri.Utils.basename(resource); const body = localize('preview.notFound', '{0} cannot be found', resourcePath); return ` @@ -154,7 +154,7 @@ export class MarkdownContentProvider { } // Assume it must be a local file - if (isAbsolute(href)) { + if (href.startsWith('/') || /^[a-z]:\\/i.test(href)) { return resourceProvider.asWebviewUri(vscode.Uri.file(href)).toString(); } @@ -165,7 +165,7 @@ export class MarkdownContentProvider { } // Otherwise look relative to the markdown file - return resourceProvider.asWebviewUri(vscode.Uri.file(join(dirname(resource.fsPath), href))).toString(); + return resourceProvider.asWebviewUri(vscode.Uri.joinPath(uri.Utils.dirname(resource), href)).toString(); } private computeCustomStyleSheetIncludes(resourceProvider: WebviewResourceProvider, resource: vscode.Uri, config: MarkdownPreviewConfiguration): string { diff --git a/extensions/markdown-language-features/src/features/previewManager.ts b/extensions/markdown-language-features/src/features/previewManager.ts index 6b7e3bdbc89..e2cda4e7820 100644 --- a/extensions/markdown-language-features/src/features/previewManager.ts +++ b/extensions/markdown-language-features/src/features/previewManager.ts @@ -171,7 +171,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview document: vscode.TextDocument, webview: vscode.WebviewPanel ): Promise { - const lineNumber = this._topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri); + const lineNumber = this._topmostLineMonitor.getPreviousStaticTextEditorLineByUri(document.uri); const preview = StaticMarkdownPreview.revive( document.uri, webview, diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 72bdba51fcb..e49122c03be 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -384,14 +384,18 @@ export class MarkdownEngine { } async function getMarkdownOptions(md: () => MarkdownIt): Promise { - const hljs = await import('highlight.js'); + const hljs = (await import('highlight.js')).default; return { html: true, highlight: (str: string, lang?: string) => { lang = normalizeHighlightLang(lang); if (lang && hljs.getLanguage(lang)) { try { - return `
${hljs.highlight(lang, str, true).value}
`; + const highlighted = hljs.highlight(str, { + language: lang, + ignoreIllegals: true, + }).value; + return `
${highlighted}
`; } catch (error) { } } diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index 2394240a1cd..2b358067946 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -151,6 +151,33 @@ suite('markdown.DocumentLinkProvider', () => { assertRangeEqual(link2.range, new vscode.Range(1, 6, 1, 8)); }); + test('Should only find one link for reference sources [a]: source (#141285)', async () => { + const links = await getLinksForFile([ + '[Works]: https://microsoft.com', + ].join('\n')); + + assert.strictEqual(links.length, 1); + }); + + test('Should find links for referenes with only one [] (#141285)', async () => { + let links = await getLinksForFile([ + '[Works]', + '[Works]: https://microsoft.com', + ].join('\n')); + assert.strictEqual(links.length, 2); + + links = await getLinksForFile([ + '[Does Not Work]', + '[Works]: https://microsoft.com', + ].join('\n')); + assert.strictEqual(links.length, 1); + }); + + test('Should not find link for reference using one [] when source does not exist (#141285)', async () => { + const links = await getLinksForFile('[Works]'); + assert.strictEqual(links.length, 0); + }); + test('Should not consider links in code fenced with backticks', async () => { const text = joinLines( '```', diff --git a/extensions/markdown-language-features/src/util/openDocumentLink.ts b/extensions/markdown-language-features/src/util/openDocumentLink.ts index 542cdbdb763..95a07e2d800 100644 --- a/extensions/markdown-language-features/src/util/openDocumentLink.ts +++ b/extensions/markdown-language-features/src/util/openDocumentLink.ts @@ -5,10 +5,10 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import * as uri from 'vscode-uri'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContents } from '../tableOfContentsProvider'; import { isMarkdownFile } from './file'; -import { extname } from './path'; export interface OpenDocumentLinkArgs { readonly parts: vscode.Uri; @@ -53,7 +53,7 @@ export async function openDocumentLink(engine: MarkdownEngine, targetResource: v if (typeof targetResourceStat === 'undefined') { // We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead - if (extname(targetResource.path) === '') { + if (uri.Utils.extname(targetResource) === '') { const dotMdResource = targetResource.with({ path: targetResource.path + '.md' }); try { const stat = await vscode.workspace.fs.stat(dotMdResource); @@ -140,7 +140,7 @@ export async function resolveUriToMarkdownFile(resource: vscode.Uri): Promise process.exit(1)); diff --git a/extensions/merge-conflict/package.json b/extensions/merge-conflict/package.json index 3aa6b16014d..128538a8ad9 100644 --- a/extensions/merge-conflict/package.json +++ b/extensions/merge-conflict/package.json @@ -159,7 +159,7 @@ "vscode-nls": "^5.0.0" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/merge-conflict/yarn.lock b/extensions/merge-conflict/yarn.lock index ede1d9c7736..699f1238a9b 100644 --- a/extensions/merge-conflict/yarn.lock +++ b/extensions/merge-conflict/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index aed500cc870..58118fefe5d 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -47,7 +47,7 @@ "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "devDependencies": { - "@types/node": "14.x", + "@types/node": "16.x", "@types/node-fetch": "^2.5.7", "@types/randombytes": "^2.0.0", "@types/sha.js": "^2.4.0", @@ -60,7 +60,7 @@ "sha.js": "2.4.11", "stream": "0.0.2", "uuid": "^8.2.0", - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-nls": "^5.0.0" }, "repository": { diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 9dec4a9aa4a..736106853fa 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -20,7 +20,8 @@ import path = require('path'); const localize = nls.loadMessageBundle(); -const redirectUrl = 'https://vscode-redirect.azurewebsites.net/'; +// TODO: Change to stable when it's deployed. +const redirectUrl = 'https://insiders.vscode.dev/redirect'; const loginEndpointUrl = 'https://login.microsoftonline.com/'; const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; const DEFAULT_TENANT = 'organizations'; @@ -87,14 +88,6 @@ interface IScopeData { tenant: string; } -function parseQuery(uri: vscode.Uri) { - return uri.query.split('&').reduce((prev: any, current) => { - const queryString = current.split('='); - prev[queryString[0]] = queryString[1]; - return prev; - }, {}); -} - export const onDidChangeSessions = new vscode.EventEmitter(); export const REFRESH_NETWORK_FAILURE = 'Network failure'; @@ -115,7 +108,7 @@ export class AzureActiveDirectoryService { private _uriHandler: UriEventHandler; // Used to keep track of current requests when not using the local server approach. - private _pendingStates = new Map(); + private _pendingNonces = new Map(); private _codeExchangePromises = new Map>(); private _codeVerfifiers = new Map(); @@ -164,6 +157,7 @@ export class AzureActiveDirectoryService { sessionId: session.id }); } else { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); Logger.error(e); await this.removeSession(session.id); } @@ -189,7 +183,7 @@ export class AzureActiveDirectoryService { return sessions; } - const modifiedScopes = [...scopes]; + let modifiedScopes = [...scopes]; if (!modifiedScopes.includes('openid')) { modifiedScopes.push('openid'); } @@ -199,9 +193,10 @@ export class AzureActiveDirectoryService { if (!modifiedScopes.includes('profile')) { modifiedScopes.push('profile'); } + modifiedScopes = modifiedScopes.sort(); - let orderedScopes = modifiedScopes.sort().join(' '); - Logger.info(`Getting sessions for the following scopes: ${orderedScopes}`); + let modifiedScopesStr = modifiedScopes.join(' '); + Logger.info(`Getting sessions for the following scopes: ${modifiedScopesStr}`); if (this._refreshingPromise) { Logger.info('Refreshing in progress. Waiting for completion before continuing.'); @@ -212,18 +207,51 @@ export class AzureActiveDirectoryService { } } - let matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); + let matchingTokens = this._tokens.filter(token => token.scope === modifiedScopesStr); // The user may still have a token that doesn't have the openid & email scopes so check for that as well. // Eventually, we should remove this and force the user to re-log in so that we don't have any sessions // without an idtoken. if (!matchingTokens.length) { - orderedScopes = scopes.sort().join(' '); - Logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${orderedScopes}`); - matchingTokens = this._tokens.filter(token => token.scope === orderedScopes); + const fallbackOrderedScopes = scopes.sort().join(' '); + Logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${fallbackOrderedScopes}`); + matchingTokens = this._tokens.filter(token => token.scope === fallbackOrderedScopes); + if (matchingTokens.length) { + modifiedScopesStr = fallbackOrderedScopes; + } } - Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${orderedScopes}`); + // If we still don't have a matching token try to get a new token from an existing token by using + // the refreshToken. This is documented here: + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token + // "Refresh tokens are valid for all permissions that your client has already received consent for." + if (!matchingTokens.length) { + const clientId = this.getClientId(modifiedScopes); + // Get a token with the correct client id. + const token = clientId === DEFAULT_CLIENT_ID + ? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID')) + : this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${clientId}`)); + + if (token) { + const scopeData: IScopeData = { + clientId, + scopes: modifiedScopes, + scopeStr: modifiedScopesStr, + // filter our special scopes + scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), + tenant: this.getTenantId(modifiedScopes), + }; + + try { + const itoken = await this.refreshToken(token.refreshToken, scopeData); + matchingTokens.push(itoken); + } catch (err) { + Logger.error(`Attempted to get a new session for scopes '${scopeData.scopeStr}' using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`); + } + } + } + + Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`); return Promise.all(matchingTokens.map(token => this.convertToSession(token))); } @@ -275,7 +303,7 @@ export class AzureActiveDirectoryService { private async createSessionWithLocalServer(scopeData: IScopeData) { const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); - const qs = querystring.stringify({ + const qs = new URLSearchParams({ response_type: 'code', response_mode: 'query', client_id: scopeData.clientId, @@ -284,11 +312,10 @@ export class AzureActiveDirectoryService { prompt: 'select_account', code_challenge_method: 'S256', code_challenge: codeChallenge, - }); + }).toString(); const loginUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`; const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); await server.start(); - server.state = `${server.port},${encodeURIComponent(server.nonce)}`; let codeToExchange; try { @@ -312,18 +339,29 @@ export class AzureActiveDirectoryService { } private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise { - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); + let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = randomBytes(16).toString('base64'); - const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); - const callbackEnvironment = AzureActiveDirectoryService.getCallbackEnvironment(callbackUri); - const state = `${callbackEnvironment},${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; - const signInUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`; - let uri = vscode.Uri.parse(signInUrl); + const callbackQuery = new URLSearchParams(callbackUri.query); + callbackQuery.set('nonce', encodeURIComponent(nonce)); + callbackUri = callbackUri.with({ + query: callbackQuery.toString() + }); + const state = encodeURIComponent(callbackUri.toString(true)); const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64')); const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier)); - uri = uri.with({ - query: `response_type=code&client_id=${encodeURIComponent(scopeData.clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scopeData.scopesToSend}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}` + const signInUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`; + const oauthStartQuery = new URLSearchParams({ + response_type: 'code', + client_id: encodeURIComponent(scopeData.clientId), + response_mode: 'query', + redirect_uri: redirectUrl, + state, + scope: scopeData.scopesToSend, + prompt: 'select_account', + code_challenge_method: 'S256', + code_challenge: codeChallenge, }); + let uri = vscode.Uri.parse(`${signInUrl}?${oauthStartQuery.toString()}`); vscode.env.openExternal(uri); const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession) => void, reject) => { @@ -333,8 +371,8 @@ export class AzureActiveDirectoryService { }, 1000 * 60 * 5); }); - const existingStates = this._pendingStates.get(scopeData.scopeStr) || []; - this._pendingStates.set(scopeData.scopeStr, [...existingStates, state]); + const existingNonces = this._pendingNonces.get(scopeData.scopeStr) || []; + this._pendingNonces.set(scopeData.scopeStr, [...existingNonces, nonce]); // Register a single listener for the URI callback, in case the user starts the login process multiple times // before completing it. @@ -344,13 +382,13 @@ export class AzureActiveDirectoryService { this._codeExchangePromises.set(scopeData.scopeStr, existingPromise); } - this._codeVerfifiers.set(state, codeVerifier); + this._codeVerfifiers.set(nonce, codeVerifier); return Promise.race([existingPromise, timeoutPromise]) .finally(() => { - this._pendingStates.delete(scopeData.scopeStr); + this._pendingNonces.delete(scopeData.scopeStr); this._codeExchangePromises.delete(scopeData.scopeStr); - this._codeVerfifiers.delete(state); + this._codeVerfifiers.delete(nonce); }); } @@ -402,6 +440,7 @@ export class AzureActiveDirectoryService { onDidChangeSessions.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] }); } catch (e) { if (e.message !== REFRESH_NETWORK_FAILURE) { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); await this.removeSession(sessionId); } } @@ -517,7 +556,7 @@ export class AzureActiveDirectoryService { //#region refresh logic - private async refreshToken(refreshToken: string, scopeData: IScopeData, sessionId: string): Promise { + private async refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { this._refreshingPromise = this.doRefreshToken(refreshToken, scopeData, sessionId); try { const result = await this._refreshingPromise; @@ -527,7 +566,7 @@ export class AzureActiveDirectoryService { } } - private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId: string): Promise { + private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { Logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`); const postData = querystring.stringify({ refresh_token: refreshToken, @@ -552,13 +591,14 @@ export class AzureActiveDirectoryService { } catch (e) { if (e.message === REFRESH_NETWORK_FAILURE) { // We were unable to refresh because of a network failure (i.e. the user lost internet access). - // so set up a timeout to try again later. - this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT); + // so set up a timeout to try again later. We only do this if we have a session id to reference later. + if (sessionId) { + this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT); + } throw e; } - vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); Logger.error(`Refreshing token failed (for scopes: ${scopeData.scopeStr}): ${e.message}`); - throw new Error('Refreshing token failed'); + throw e; } } @@ -593,15 +633,29 @@ export class AzureActiveDirectoryService { return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => { uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { try { - const query = parseQuery(uri); - const code = query.code; - const acceptedStates = this._pendingStates.get(scopeData.scopeStr) || []; - // Workaround double encoding issues of state in web - if (!acceptedStates.includes(query.state) && !acceptedStates.includes(decodeURIComponent(query.state))) { - throw new Error('State does not match.'); + console.log(uri.query); + const query = querystring.parse(uri.query); + let { code, nonce } = query; + if (Array.isArray(code)) { + code = code[0]; + } + if (!code) { + throw new Error('No code included in query'); + } + if (Array.isArray(nonce)) { + nonce = nonce[0]; + } + if (!nonce) { + throw new Error('No nonce included in query'); } - const verifier = this._codeVerfifiers.get(query.state) ?? this._codeVerfifiers.get(decodeURIComponent(query.state)); + const acceptedStates = this._pendingNonces.get(scopeData.scopeStr) || []; + // Workaround double encoding issues of state in web + if (!acceptedStates.includes(nonce) && !acceptedStates.includes(decodeURIComponent(nonce))) { + throw new Error('Nonce does not match.'); + } + + const verifier = this._codeVerfifiers.get(nonce) ?? this._codeVerfifiers.get(decodeURIComponent(nonce)); if (!verifier) { throw new Error('No available code verifier'); } @@ -743,6 +797,7 @@ export class AzureActiveDirectoryService { } catch (e) { // Network failures will automatically retry on next poll. if (e.message !== REFRESH_NETWORK_FAILURE) { + vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); await this.removeSession(session.id); } return; diff --git a/extensions/microsoft-authentication/src/authServer.ts b/extensions/microsoft-authentication/src/authServer.ts index 158d0db257f..c36e56175de 100644 --- a/extensions/microsoft-authentication/src/authServer.ts +++ b/extensions/microsoft-authentication/src/authServer.ts @@ -155,6 +155,10 @@ export class LoopbackAuthServer implements ILoopbackServer { } clearTimeout(portTimeout); + + // set state which will be used to redirect back to vscode + this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`; + resolve(this.port); }); this._server.on('error', err => { diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 6de9590bdc8..4b9d06d1847 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -11,7 +11,10 @@ "resolveJsonModule": true, "rootDir": "src", "skipLibCheck": true, - "sourceMap": true + "sourceMap": true, + "lib": [ + "WebWorker" + ] }, "exclude": [ "node_modules" diff --git a/extensions/microsoft-authentication/yarn.lock b/extensions/microsoft-authentication/yarn.lock index ca07545f9e3..b3ad2657f88 100644 --- a/extensions/microsoft-authentication/yarn.lock +++ b/extensions/microsoft-authentication/yarn.lock @@ -15,10 +15,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.23.tgz#676fa0883450ed9da0bb24156213636290892806" integrity sha512-Z4U8yDAl5TFkmYsZdFPdjeMa57NOvnaf1tljHzhouaPEp7LCj2JKkejpI1ODviIAQuW4CcQmxkQ77rnLsOOoKw== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/randombytes@^2.0.0": version "2.0.0" @@ -39,10 +39,10 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== asynckit@^0.4.0: version "0.4.0" diff --git a/extensions/notebook-renderers/esbuild.js b/extensions/notebook-renderers/esbuild.js index 5b54e668b3b..11a04a0a982 100644 --- a/extensions/notebook-renderers/esbuild.js +++ b/extensions/notebook-renderers/esbuild.js @@ -28,5 +28,6 @@ esbuild.build({ outdir: outDir, platform: 'browser', target: ['es2020'], + watch: isWatch, incremental: isWatch, }).catch(() => process.exit(1)); diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 8f631774f26..61c948aaa76 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -138,6 +138,9 @@ function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boo const text = outputInfo.text(); truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element); + while (container.firstChild) { + container.removeChild(container.firstChild); + } container.appendChild(element); container.setAttribute('output-mime-type', outputInfo.mime); if (error) { @@ -173,6 +176,22 @@ export const activate: ActivationFunction = (ctx) => { -ms-user-select: text; cursor: auto; } + .output-plaintext .code-bold, + .output-stream .code-bold { + font-weight: bold; + } + .output-plaintext .code-italic, + .output-stream .code-italic { + font-style: italic; + } + .output-plaintext .code-strike-through, + .output-stream .code-strike-through { + text-decoration: line-through; + } + .output-plaintext .code-underline, + .output-stream .code-underline { + text-decoration: underline; + } `; document.body.appendChild(style); return { diff --git a/extensions/npm/package.json b/extensions/npm/package.json index b674260dd5b..6df1a4835e7 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@types/minimatch": "^3.0.3", - "@types/node": "14.x", + "@types/node": "16.x", "@types/which": "^2.0.0" }, "resolutions": { diff --git a/extensions/npm/yarn.lock b/extensions/npm/yarn.lock index 4728dbc860c..bba9ffdbf39 100644 --- a/extensions/npm/yarn.lock +++ b/extensions/npm/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/which@^2.0.0": version "2.0.0" diff --git a/extensions/package.json b/extensions/package.json index 6d8209dd5f4..f27f85fefe6 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,13 +4,13 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.6.1-rc" + "typescript": "4.6.2" }, "scripts": { - "postinstall": "node ./postinstall" + "postinstall": "node ./postinstall.mjs" }, "devDependencies": { "esbuild": "^0.11.12", - "vscode-grammar-updater": "^1.0.3" + "vscode-grammar-updater": "^1.0.4" } } diff --git a/extensions/php-language-features/package.json b/extensions/php-language-features/package.json index 03176a05573..8c0d484535b 100644 --- a/extensions/php-language-features/package.json +++ b/extensions/php-language-features/package.json @@ -78,7 +78,7 @@ "which": "^2.0.2" }, "devDependencies": { - "@types/node": "14.x", + "@types/node": "16.x", "@types/which": "^2.0.0" }, "repository": { diff --git a/extensions/php-language-features/src/features/validationProvider.ts b/extensions/php-language-features/src/features/validationProvider.ts index bc8b7d25966..6a494b3231a 100644 --- a/extensions/php-language-features/src/features/validationProvider.ts +++ b/extensions/php-language-features/src/features/validationProvider.ts @@ -112,7 +112,9 @@ export default class PHPValidationProvider { vscode.workspace.onDidOpenTextDocument(this.triggerValidate, this, subscriptions); vscode.workspace.onDidCloseTextDocument((textDocument) => { this.diagnosticCollection!.delete(textDocument.uri); - delete this.delayers![textDocument.uri.toString()]; + if (this.delayers) { + delete this.delayers[textDocument.uri.toString()]; + } }, null, subscriptions); } diff --git a/extensions/php-language-features/yarn.lock b/extensions/php-language-features/yarn.lock index 09b31c80123..8dec3aadc63 100644 --- a/extensions/php-language-features/yarn.lock +++ b/extensions/php-language-features/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/which@^2.0.0": version "2.0.0" diff --git a/extensions/php/build/update-grammar.js b/extensions/php/build/update-grammar.mjs similarity index 92% rename from extensions/php/build/update-grammar.js rename to extensions/php/build/update-grammar.mjs index 4216d25841f..5fa17f218af 100644 --- a/extensions/php/build/update-grammar.js +++ b/extensions/php/build/update-grammar.mjs @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; +//@ts-check -const updateGrammar = require('vscode-grammar-updater'); +import * as vscodeGrammarUpdater from 'vscode-grammar-updater'; function adaptInjectionScope(grammar) { // we're using the HTML grammar from https://github.com/textmate/html.tmbundle which has moved away from source.js.embedded.html @@ -68,8 +68,8 @@ function fixBadRegex(grammar) { } } -updateGrammar.update('atom/language-php', 'grammars/php.cson', './syntaxes/php.tmLanguage.json', fixBadRegex); -updateGrammar.update('atom/language-php', 'grammars/html.cson', './syntaxes/html.tmLanguage.json', grammar => { +vscodeGrammarUpdater.update('atom/language-php', 'grammars/php.cson', './syntaxes/php.tmLanguage.json', fixBadRegex); +vscodeGrammarUpdater.update('atom/language-php', 'grammars/html.cson', './syntaxes/html.tmLanguage.json', grammar => { adaptInjectionScope(grammar); includeDerivativeHtml(grammar); }); diff --git a/extensions/php/package.json b/extensions/php/package.json index 7842141ffaf..145431f9318 100644 --- a/extensions/php/package.json +++ b/extensions/php/package.json @@ -59,7 +59,7 @@ ] }, "scripts": { - "update-grammar": "node ./build/update-grammar.js" + "update-grammar": "node ./build/update-grammar.mjs" }, "repository": { "type": "git", diff --git a/extensions/postinstall.js b/extensions/postinstall.mjs similarity index 84% rename from extensions/postinstall.js rename to extensions/postinstall.mjs index da4fa3e9d04..110b9b3b476 100644 --- a/extensions/postinstall.js +++ b/extensions/postinstall.mjs @@ -2,15 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// @ts-check -'use strict'; +import * as fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; -const fs = require('fs'); -const path = require('path'); -const rimraf = require('rimraf'); - -const root = path.join(__dirname, 'node_modules', 'typescript'); +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), 'node_modules', 'typescript'); function processRoot() { const toKeep = new Set([ @@ -21,7 +18,7 @@ function processRoot() { if (!toKeep.has(name)) { const filePath = path.join(root, name); console.log(`Removed ${filePath}`); - rimraf.sync(filePath); + fs.rmSync(filePath, { recursive: true }); } } } diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index e9ce7c7e783..6e5db2be9b0 100644 --- a/extensions/r/cgmanifest.json +++ b/extensions/r/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "Ikuyadeu/vscode-R", "repositoryUrl": "https://github.com/Ikuyadeu/vscode-R", - "commitHash": "d968decca543045fb3488d62a27ff2ecfa3c40c4" + "commitHash": "ff60e426f66503f3c9533c7a62a8fd3f9f6c53df" } }, "license": "MIT", - "version": "2.3.5" + "version": "2.3.8" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index 68cc06bb306..90df4dedb79 100644 --- a/extensions/r/syntaxes/r.tmLanguage.json +++ b/extensions/r/syntaxes/r.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Ikuyadeu/vscode-R/commit/d968decca543045fb3488d62a27ff2ecfa3c40c4", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/ff60e426f66503f3c9533c7a62a8fd3f9f6c53df", "name": "R", "scopeName": "source.r", "patterns": [ @@ -437,7 +437,7 @@ "function-declarations": { "patterns": [ { - "match": "((?:`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`)|(?:[[:alpha:].][[:alnum:]._]*))\\s*("], + ["[", "]"] + ], + "surroundingPairs": [ + ["(", ")"], + ["<", ">"], + ["`", "`"], + ["*", "*"], + ["|", "|"], + ["[", "]"] + ], + "autoClosingPairs": [ + { "open": "(", "close": ")" }, + { "open": "<", "close": ">" }, + { "open": "'", "close": "'"}, + { "open": "`", "close": "`", "notIn": ["string"]}, + { "open": "\"", "close": "\""}, + { "open": "[", "close": "]"} + ], + "autoCloseBefore": ":})>`\\n ", + "onEnterRules": [ + { + "beforeText": "^\\s*\\.\\. *$|(?" + }, + { + "match": "{[`*]+}" + }, + { + "match": "\\([`*]+\\)" + }, + { + "match": "\\[[`*]+\\]" + }, + { + "match": "\"[`*]+\"" + } + ] + }, + "table": { + "begin": "^\\s*\\+[=+-]+\\+\\s*$", + "end": "^(?![+|])", + "beginCaptures": { + "0": { + "name": "keyword.control.table" + } + }, + "patterns": [ + { + "match": "[=+|-]", + "name": "keyword.control.table" + } + ] + }, + "simple-table": { + "match": "^[=\\s]+$", + "name": "keyword.control.table" + }, + "ref": { + "begin": "(:ref:)`", + "end": "`|^\\s*$", + "name": "entity.name.tag", + "beginCaptures": { + "1": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "match": "<.*?>", + "name": "markup.underline.link" + } + ] + }, + "reference": { + "match": "[\\w-]*[a-zA-Z\\d-]__?\\b", + "name": "entity.name.tag" + }, + "macro": { + "match": "\\|[^\\|]+\\|", + "name": "entity.name.tag" + }, + "literal": { + "match": "(:\\S+:)(`.*?`\\\\?)", + "captures": { + "1": { + "name": "keyword.control" + }, + "2": { + "name": "entity.name.tag" + } + } + }, + "monospaced": { + "begin": "(?<=[\\s\"'(\\[{<]|^)``[^\\s`]", + "end": "``|^\\s*$", + "name": "string.interpolated" + }, + "citation": { + "begin": "(?<=[\\s\"'(\\[{<]|^)`[^\\s`]", + "end": "`_{,2}|^\\s*$", + "name": "entity.name.tag", + "applyEndPatternLast": 0 + }, + "bold": { + "begin": "(?<=[\\s\"'(\\[{<]|^)\\*{2}[^\\s*]", + "end": "\\*{2}|^\\s*$", + "name": "markup.bold" + }, + "italic": { + "begin": "(?<=[\\s\"'(\\[{<]|^)\\*[^\\s*]", + "end": "\\*|^\\s*$", + "name": "markup.italic" + }, + "escaped": { + "match": "\\\\.", + "name": "constant.character.escape" + }, + "list": { + "match": "^\\s*(\\d+\\.|\\* -|[a-zA-Z#]\\.|[iIvVxXmMcC]+\\.|\\(\\d+\\)|\\d+\\)|[*+-])\\s+", + "name": "keyword.control" + }, + "line-block": { + "match": "^\\|\\s+", + "name": "keyword.control" + }, + "raw-html": { + "begin": "^(\\s*)(\\.{2}\\s+raw\\s*::)\\s+(html)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "3": { + "name": "variable.parameter.html" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "text.html.derivative" + } + ] + }, + "anchor": { + "match": "^\\.{2}\\s+(_[^:]+:)\\s*", + "name": "entity.name.tag.anchor" + }, + "replace-include": { + "match": "^\\s*(\\.{2})\\s+(\\|[^\\|]+\\|)\\s+(replace::)", + "captures": { + "1": { + "name": "keyword.control" + }, + "2": { + "name": "entity.name.tag" + }, + "3": { + "name": "keyword.control" + } + } + }, + "footnote": { + "match": "^\\s*\\.{2}\\s+\\[(?:[\\w\\.-]+|[#*]|#\\w+)\\]\\s+", + "name": "entity.name.tag" + }, + "footnote-ref": { + "match": "\\[(?:[\\w\\.-]+|[#*])\\]_", + "name": "entity.name.tag" + }, + "substitution": { + "match": "^\\.{2}\\s*\\|([^|]+)\\|", + "name": "entity.name.tag" + }, + "options-list": { + "match": "^((?:-\\w|--[\\w-]+|/\\w+)(?:,? ?[\\w-]+)*)(?: |\\t|$)", + "name": "variable.parameter" + }, + "blocks": { + "patterns": [ + { + "include": "#domains" + }, + { + "include": "#doctest" + }, + { + "include": "#code-block-cpp" + }, + { + "include": "#code-block-py" + }, + { + "include": "#code-block-console" + }, + { + "include": "#code-block-javascript" + }, + { + "include": "#code-block-yaml" + }, + { + "include": "#code-block-cmake" + }, + { + "include": "#code-block-kconfig" + }, + { + "include": "#code-block-ruby" + }, + { + "include": "#code-block-dts" + }, + { + "include": "#code-block" + }, + { + "include": "#doctest-block" + }, + { + "include": "#raw-html" + }, + { + "include": "#block" + }, + { + "include": "#literal-block" + }, + { + "include": "#block-comment" + } + ] + }, + "block-comment": { + "begin": "^(\\s*)\\.{2}", + "while": "^\\1(?=\\s)|^\\s*$", + "name": "comment.block" + }, + "literal-block": { + "begin": "^(\\s*)(.*)(::)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "patterns": [ + { + "include": "#inline-markup" + } + ] + }, + "3": { + "name": "keyword.control" + } + } + }, + "block": { + "begin": "^(\\s*)(\\.{2}\\s+\\S+::)(.*)", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "3": { + "name": "variable" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "#body" + } + ] + }, + "block-param": { + "patterns": [ + { + "match": "(:param\\s+(.+?):)(?:\\s|$)", + "captures": { + "1": { + "name": "keyword.control" + }, + "2": { + "name": "variable.parameter" + } + } + }, + { + "match": "(:.+?:)(?:$|\\s+(.*))", + "captures": { + "1": { + "name": "keyword.control" + }, + "2": { + "patterns": [ + { + "match": "\\b(0x[a-fA-F\\d]+|\\d+)\\b", + "name": "constant.numeric" + }, + { + "include": "#inline-markup" + } + ] + } + } + } + ] + }, + "domains": { + "patterns": [ + { + "include": "#domain-cpp" + }, + { + "include": "#domain-py" + }, + { + "include": "#domain-auto" + }, + { + "include": "#domain-js" + } + ] + }, + "domain-cpp": { + "begin": "^(\\s*)(\\.{2}\\s+(?:cpp|c):(?:class|struct|function|member|var|type|enum|enum-struct|enum-class|enumerator|union|concept)::)\\s*(?:(@\\w+)|(.*))", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "3": { + "name": "entity.name.tag" + }, + "4": { + "patterns": [ + { + "include": "source.cpp" + } + ] + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "#body" + } + ] + }, + "domain-py": { + "begin": "^(\\s*)(\\.{2}\\s+py:(?:module|function|data|exception|class|attribute|property|method|staticmethod|classmethod|decorator|decoratormethod)::)\\s*(.*)", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "3": { + "patterns": [ + { + "include": "source.python" + } + ] + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "#body" + } + ] + }, + "domain-auto": { + "begin": "^(\\s*)(\\.{2}\\s+auto(?:class|module|exception|function|decorator|data|method|attribute|property)::)\\s*(.*)", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control.py" + }, + "3": { + "patterns": [ + { + "include": "source.python" + } + ] + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "#body" + } + ] + }, + "domain-js": { + "begin": "^(\\s*)(\\.{2}\\s+js:\\w+::)\\s*(.*)", + "end": "^(?!\\1[ \\t]|$)", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "3": { + "patterns": [ + { + "include": "source.js" + } + ] + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "#body" + } + ] + }, + "doctest": { + "begin": "^(>>>)\\s*(.*)", + "end": "^\\s*$", + "beginCaptures": { + "1": { + "name": "keyword.control" + }, + "2": { + "patterns": [ + { + "include": "source.python" + } + ] + } + } + }, + "code-block-cpp": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(c|c\\+\\+|cpp|C|C\\+\\+|CPP|Cpp)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.cpp" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.cpp" + } + ] + }, + "code-block-console": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(console|shell|bash)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.console" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.shell" + } + ] + }, + "code-block-py": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(python)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.py" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.python" + } + ] + }, + "code-block-javascript": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(javascript)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.js" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.js" + } + ] + }, + "code-block-yaml": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(ya?ml)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.yaml" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.yaml" + } + ] + }, + "code-block-cmake": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(cmake)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.cmake" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.cmake" + } + ] + }, + "code-block-kconfig": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*([kK]config)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.kconfig" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.kconfig" + } + ] + }, + "code-block-ruby": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(ruby)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.ruby" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.ruby" + } + ] + }, + "code-block-dts": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)\\s*(dts|DTS|devicetree)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + }, + "4": { + "name": "variable.parameter.codeblock.dts" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.dts" + } + ] + }, + "code-block": { + "begin": "^(\\s*)(\\.{2}\\s+(code|code-block)::)", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#block-param" + } + ] + }, + "doctest-block": { + "begin": "^(\\s*)(\\.{2}\\s+doctest::)\\s*$", + "while": "^\\1(?=\\s)|^\\s*$", + "beginCaptures": { + "2": { + "name": "keyword.control" + } + }, + "patterns": [ + { + "include": "#block-param" + }, + { + "include": "source.python" + } + ] + } + } +} \ No newline at end of file diff --git a/extensions/restructuredtext/yarn.lock b/extensions/restructuredtext/yarn.lock new file mode 100644 index 00000000000..fb57ccd13af --- /dev/null +++ b/extensions/restructuredtext/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/extensions/simple-browser/esbuild-preview.js b/extensions/simple-browser/esbuild-preview.js new file mode 100644 index 00000000000..d6447532501 --- /dev/null +++ b/extensions/simple-browser/esbuild-preview.js @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const path = require('path'); +const fs = require('fs'); +const esbuild = require('esbuild'); + +const args = process.argv.slice(2); + +const isWatch = args.indexOf('--watch') >= 0; + +let outputRoot = __dirname; +const outputRootIndex = args.indexOf('--outputRoot'); +if (outputRootIndex >= 0) { + outputRoot = args[outputRootIndex + 1]; +} + +const outDir = path.join(outputRoot, 'media'); + +fs.copyFileSync( + path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.css'), + path.join(outDir, 'codicon.css')); + +fs.copyFileSync( + path.join(__dirname, 'node_modules', 'vscode-codicons', 'dist', 'codicon.ttf'), + path.join(outDir, 'codicon.ttf')); + +esbuild.build({ + entryPoints: [ + path.join(__dirname, 'preview-src', 'index.ts') + ], + bundle: true, + minify: true, + sourcemap: false, + format: 'esm', + outdir: outDir, + platform: 'browser', + target: ['es2020'], + watch: isWatch, + incremental: isWatch, +}).catch(() => process.exit(1)); diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 6ce11704fa4..711ffbd81e3 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -62,13 +62,12 @@ "watch": "npm run build-preview && gulp watch-extension:markdown-language-features", "vscode:prepublish": "npm run build-ext && npm run build-preview", "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json", - "build-preview": "npx webpack-cli --mode development", - "build-preview-production": "npx webpack-cli --mode production", + "build-preview": "node ./esbuild-preview", "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" }, "dependencies": { - "@vscode/extension-telemetry": "0.4.6", + "@vscode/extension-telemetry": "0.4.10", "vscode-nls": "^5.0.0" }, "devDependencies": { diff --git a/extensions/simple-browser/webpack.config.js b/extensions/simple-browser/webpack.config.js deleted file mode 100644 index fbdf2a6c597..00000000000 --- a/extensions/simple-browser/webpack.config.js +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const path = require('path'); -const CopyPlugin = require('copy-webpack-plugin'); - -module.exports = { - context: path.resolve(__dirname), - entry: { - index: './preview-src/index.ts', - }, - mode: 'production', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/ - } - ] - }, - resolve: { - extensions: ['.tsx', '.ts', '.js'] - }, - output: { - filename: '[name].js', - path: path.resolve(__dirname, 'media') - }, - plugins: [ - // @ts-ignore - new CopyPlugin({ - patterns: [ - { - from: './node_modules/vscode-codicons/dist/codicon.css', - to: 'codicon.css' - }, - { - from: './node_modules/vscode-codicons/dist/codicon.ttf', - to: 'codicon.ttf' - }, - ], - }), - ] -}; diff --git a/extensions/simple-browser/yarn.lock b/extensions/simple-browser/yarn.lock index 54a54b18ae4..af6b2add33e 100644 --- a/extensions/simple-browser/yarn.lock +++ b/extensions/simple-browser/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf" integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== vscode-codicons@^0.0.14: version "0.0.14" diff --git a/extensions/sql/build/update-grammar.mjs b/extensions/sql/build/update-grammar.mjs new file mode 100644 index 00000000000..40e2102e1e4 --- /dev/null +++ b/extensions/sql/build/update-grammar.mjs @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscodeGrammarUpdater from 'vscode-grammar-updater'; + +vscodeGrammarUpdater.update('microsoft/vscode-mssql', 'syntaxes/SQL.plist', './syntaxes/sql.tmLanguage.json', undefined, 'main'); diff --git a/extensions/sql/package.json b/extensions/sql/package.json index ed0f77a4e37..20e19ad8dd0 100644 --- a/extensions/sql/package.json +++ b/extensions/sql/package.json @@ -9,7 +9,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ./build/update-grammar.js" + "update-grammar": "node ./build/update-grammar.mjs" }, "contributes": { "languages": [ diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index be1b132172e..f63b127dc62 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/textmate/swift.tmbundle/commit/8c7672d74c1baa4e6944a05ac6c57a623532f18b", + "version": "https://github.com/textmate/swift.tmbundle/commit/7a35637eb70aef3114b091c4ff6fbf6a2faa881b", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -109,7 +109,7 @@ "name": "invalid.illegal.character-not-allowed-here.swift" } }, - "match": "(?:(\\*)|\\b(deprecated|unavailable)\\b)\\s*(.*?)(?=[,)])" + "match": "(?:(\\*)|\\b(deprecated|unavailable|noasync)\\b)\\s*(.*?)(?=[,)])" } ] }, @@ -818,7 +818,11 @@ "name": "keyword.operator.type.opaque.swift" }, { - "match": "\\binout\\b", + "match": "\\bany\\b", + "name": "keyword.operator.type.existential.swift" + }, + { + "match": "\\b(?:inout|isolated)\\b", "name": "storage.modifier.swift" }, { @@ -986,19 +990,22 @@ ] }, "function": { - "begin": "(?x)\n\t\t\t\t\t\t\\b\n\t\t\t\t\t\t(func)\n\t\t\t\t\t\t\\s+\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\t(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n\t\t\t\t\t\t | (?:\n\t\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t\t(?\t\t\t\t\t\t\t\t# operator-head\n\t\t\t\t\t\t\t\t\t\t[/=\\-+!*%<>&|^~?]\n\t\t\t\t\t\t\t\t\t | [\\x{00A1}-\\x{00A7}]\n\t\t\t\t\t\t\t\t\t | [\\x{00A9}\\x{00AB}]\n\t\t\t\t\t\t\t\t\t | [\\x{00AC}\\x{00AE}]\n\t\t\t\t\t\t\t\t\t | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n\t\t\t\t\t\t\t\t\t | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n\t\t\t\t\t\t\t\t\t | [\\x{2030}-\\x{203E}]\n\t\t\t\t\t\t\t\t\t | [\\x{2041}-\\x{2053}]\n\t\t\t\t\t\t\t\t\t | [\\x{2055}-\\x{205E}]\n\t\t\t\t\t\t\t\t\t | [\\x{2190}-\\x{23FF}]\n\t\t\t\t\t\t\t\t\t | [\\x{2500}-\\x{2775}]\n\t\t\t\t\t\t\t\t\t | [\\x{2794}-\\x{2BFF}]\n\t\t\t\t\t\t\t\t\t | [\\x{2E00}-\\x{2E7F}]\n\t\t\t\t\t\t\t\t\t | [\\x{3001}-\\x{3003}]\n\t\t\t\t\t\t\t\t\t | [\\x{3008}-\\x{3030}]\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t\t\t\\g\n\t\t\t\t\t\t\t\t\t | (?\t\t\t\t\t\t\t\t# operator-character\n\t\t\t\t\t\t\t\t\t\t\t[\\x{0300}-\\x{036F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{1DC0}-\\x{1DFF}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{20D0}-\\x{20FF}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{FE00}-\\x{FE0F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{FE20}-\\x{FE2F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{E0100}-\\x{E01EF}]\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t)*\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t | ( \\. ( \\g | \\g | \\. )+ )\t\t\t# Dot operators\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\\s*\n\t\t\t\t\t\t(?=\\(|<)\n\t\t\t\t\t", + "begin": "(?x)\n\t\t\t\t\t\t\\b\n\t\t\t\t\t\t(?:(nonisolated)\\s+)?\n\t\t\t\t\t\t(func)\n\t\t\t\t\t\t\\s+\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\t(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\n\t\t\t\t\t\t | (?:\n\t\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t\t(?\t\t\t\t\t\t\t\t# operator-head\n\t\t\t\t\t\t\t\t\t\t[/=\\-+!*%<>&|^~?]\n\t\t\t\t\t\t\t\t\t | [\\x{00A1}-\\x{00A7}]\n\t\t\t\t\t\t\t\t\t | [\\x{00A9}\\x{00AB}]\n\t\t\t\t\t\t\t\t\t | [\\x{00AC}\\x{00AE}]\n\t\t\t\t\t\t\t\t\t | [\\x{00B0}-\\x{00B1}\\x{00B6}\\x{00BB}\\x{00BF}\\x{00D7}\\x{00F7}]\n\t\t\t\t\t\t\t\t\t | [\\x{2016}-\\x{2017}\\x{2020}-\\x{2027}]\n\t\t\t\t\t\t\t\t\t | [\\x{2030}-\\x{203E}]\n\t\t\t\t\t\t\t\t\t | [\\x{2041}-\\x{2053}]\n\t\t\t\t\t\t\t\t\t | [\\x{2055}-\\x{205E}]\n\t\t\t\t\t\t\t\t\t | [\\x{2190}-\\x{23FF}]\n\t\t\t\t\t\t\t\t\t | [\\x{2500}-\\x{2775}]\n\t\t\t\t\t\t\t\t\t | [\\x{2794}-\\x{2BFF}]\n\t\t\t\t\t\t\t\t\t | [\\x{2E00}-\\x{2E7F}]\n\t\t\t\t\t\t\t\t\t | [\\x{3001}-\\x{3003}]\n\t\t\t\t\t\t\t\t\t | [\\x{3008}-\\x{3030}]\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t(\n\t\t\t\t\t\t\t\t\t\t\\g\n\t\t\t\t\t\t\t\t\t | (?\t\t\t\t\t\t\t\t# operator-character\n\t\t\t\t\t\t\t\t\t\t\t[\\x{0300}-\\x{036F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{1DC0}-\\x{1DFF}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{20D0}-\\x{20FF}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{FE00}-\\x{FE0F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{FE20}-\\x{FE2F}]\n\t\t\t\t\t\t\t\t\t\t | [\\x{E0100}-\\x{E01EF}]\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t)*\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t | ( \\. ( \\g | \\g | \\. )+ )\t\t\t# Dot operators\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\\s*\n\t\t\t\t\t\t(?=\\(|<)\n\t\t\t\t\t", "beginCaptures": { "1": { - "name": "storage.type.function.swift" + "name": "storage.modifier.swift" }, "2": { - "name": "entity.name.function.swift" + "name": "storage.type.function.swift" }, "3": { - "name": "punctuation.definition.identifier.swift" + "name": "entity.name.function.swift" }, "4": { "name": "punctuation.definition.identifier.swift" + }, + "5": { + "name": "punctuation.definition.identifier.swift" } }, "end": "(?<=\\})|$(?# functions in protocol declarations or generated interfaces have no body)", @@ -2253,9 +2260,12 @@ ] }, "typed-variable-declaration": { - "begin": "(?x)\n\t\t\t\t\t\t\\b(let|var)\\b\\s+\n\t\t\t\t\t\t(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\\s*\n\t\t\t\t\t\t:\n\t\t\t\t\t", + "begin": "(?x)\n\t\t\t\t\t\t\\b(?:(async)\\s+)?(let|var)\\b\\s+\n\t\t\t\t\t\t(?`?)[\\p{L}_][\\p{L}_\\p{N}\\p{M}]*(\\k)\\s*\n\t\t\t\t\t\t:\n\t\t\t\t\t", "beginCaptures": { "1": { + "name": "keyword.control.async.swift" + }, + "2": { "name": "keyword.other.declaration-specifier.swift" } }, @@ -2641,7 +2651,21 @@ "match": "(? { - if (pattern.match && pattern.match.match(/\b(HTMLElement|ATTRIBUTE_NODE|stopImmediatePropagation)\b/g)) { + if (pattern.match && ( + /\b(HTMLElement|ATTRIBUTE_NODE|stopImmediatePropagation)\b/g.test(pattern.match) + || /\bJSON\b/g.test(pattern.match) + || /\bMath\b/g.test(pattern.match) + )) { return false; } + + if (pattern.name?.startsWith('support.class.error.') + || pattern.name?.startsWith('support.class.builtin.') + || pattern.name?.startsWith('support.function.') + ) { + return false; + } + return true; }); return grammar; @@ -78,7 +89,7 @@ function adaptToJavaScript(grammar, replacementScope) { } var tsGrammarRepo = 'microsoft/TypeScript-TmLanguage'; -updateGrammar.update(tsGrammarRepo, 'TypeScript.tmLanguage', './syntaxes/TypeScript.tmLanguage.json', grammar => patchGrammar(grammar)); -updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', './syntaxes/TypeScriptReact.tmLanguage.json', grammar => patchGrammar(grammar)); -updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScript.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js')); -updateGrammar.update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScriptReact.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js.jsx')); +update(tsGrammarRepo, 'TypeScript.tmLanguage', './syntaxes/TypeScript.tmLanguage.json', grammar => patchGrammar(grammar)); +update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', './syntaxes/TypeScriptReact.tmLanguage.json', grammar => patchGrammar(grammar)); +update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScript.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js')); +update(tsGrammarRepo, 'TypeScriptReact.tmLanguage', '../javascript/syntaxes/JavaScriptReact.tmLanguage.json', grammar => adaptToJavaScript(patchGrammar(grammar), '.js.jsx')); diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index 0f51d88cf07..2695c089993 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -10,7 +10,7 @@ "vscode": "*" }, "scripts": { - "update-grammar": "node ./build/update-grammars.js" + "update-grammar": "node ./build/update-grammars.mjs" }, "contributes": { "languages": [ diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index 5ff736e03dc..950e0afeeec 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json @@ -3615,59 +3615,10 @@ "name": "variable.language.arguments.ts", "match": "(?(); - public constructor( protected client: ITypeScriptServiceClient, private cachedResponse: CachedResponse ) { } - public get onDidChangeCodeLenses(): vscode.Event { - return this.onDidChangeCodeLensesEmitter.event; - } async provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { const filepath = this.client.toOpenedFilePath(document); @@ -58,61 +51,27 @@ export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensP return []; } - const tree = response.body; const referenceableSpans: vscode.Range[] = []; - if (tree && tree.childItems) { - tree.childItems.forEach(item => this.walkNavTree(document, item, null, referenceableSpans)); - } + response.body?.childItems?.forEach(item => this.walkNavTree(document, item, undefined, referenceableSpans)); return referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span)); } protected abstract extractSymbol( - document: vscode.TextDocument, item: Proto.NavigationTree, - parent: Proto.NavigationTree | null - ): vscode.Range | null; + parent: Proto.NavigationTree | undefined + ): vscode.Range | undefined; private walkNavTree( document: vscode.TextDocument, item: Proto.NavigationTree, - parent: Proto.NavigationTree | null, + parent: Proto.NavigationTree | undefined, results: vscode.Range[] ): void { - if (!item) { - return; - } - - const range = this.extractSymbol(document, item, parent); + const range = this.extractSymbol(item, parent); if (range) { results.push(range); } - (item.childItems || []).forEach(child => this.walkNavTree(document, child, item, results)); + item.childItems?.forEach(child => this.walkNavTree(document, child, item, results)); } } - -export function getSymbolRange( - document: vscode.TextDocument, - item: Proto.NavigationTree -): vscode.Range | null { - if (item.nameSpan) { - return typeConverters.Range.fromTextSpan(item.nameSpan); - } - - // In older versions, we have to calculate this manually. See #23924 - const span = item.spans && item.spans[0]; - if (!span) { - return null; - } - - const range = typeConverters.Range.fromTextSpan(span); - const text = document.getText(range); - - const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, 'gm'); - const match = identifierMatch.exec(text); - const prefixLength = match ? match.index + match[1].length : 0; - const startOffset = document.offsetAt(new vscode.Position(range.start.line, range.start.character)) + prefixLength; - return new vscode.Range( - document.positionAt(startOffset), - document.positionAt(startOffset + item.text.length)); -} diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index 3e510c793af..4583d6d25ec 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -13,7 +13,7 @@ import { conditionalRegistration, requireGlobalConfiguration, requireSomeCapabil import { DocumentSelector } from '../../utils/documentSelector'; import { LanguageDescription } from '../../utils/languageDescription'; import * as typeConverters from '../../utils/typeConverters'; -import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; +import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -66,25 +66,30 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip } protected extractSymbol( - document: vscode.TextDocument, item: Proto.NavigationTree, - _parent: Proto.NavigationTree | null - ): vscode.Range | null { + _parent: Proto.NavigationTree | undefined + ): vscode.Range | undefined { + if (!item.nameSpan) { + return undefined; + } + + const itemSpan = typeConverters.Range.fromTextSpan(item.nameSpan); + switch (item.kind) { case PConst.Kind.interface: - return getSymbolRange(document, item); + return itemSpan; case PConst.Kind.class: case PConst.Kind.method: case PConst.Kind.memberVariable: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: - if (item.kindModifiers.match(/\babstract\b/g)) { - return getSymbolRange(document, item); + if (/\babstract\b/g.test(item.kindModifiers)) { + return itemSpan; } break; } - return null; + return undefined; } } diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts index e3280c2387f..6f1cf9ca1ce 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts @@ -14,7 +14,7 @@ import { conditionalRegistration, requireGlobalConfiguration, requireSomeCapabil import { DocumentSelector } from '../../utils/documentSelector'; import { LanguageDescription } from '../../utils/languageDescription'; import * as typeConverters from '../../utils/typeConverters'; -import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; +import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -61,19 +61,24 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens } protected extractSymbol( - document: vscode.TextDocument, item: Proto.NavigationTree, - parent: Proto.NavigationTree | null - ): vscode.Range | null { + parent: Proto.NavigationTree | undefined + ): vscode.Range | undefined { + if (!item.nameSpan) { + return undefined; + } + + const itemSpan = typeConverters.Range.fromTextSpan(item.nameSpan); + if (parent && parent.kind === PConst.Kind.enum) { - return getSymbolRange(document, item); + return itemSpan; } switch (item.kind) { case PConst.Kind.function: { const showOnAllFunctions = vscode.workspace.getConfiguration(this.language.id).get('referencesCodeLens.showOnAllFunctions'); if (showOnAllFunctions) { - return getSymbolRange(document, item); + return itemSpan; } } // fallthrough @@ -83,7 +88,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.variable: // Only show references for exported variables if (/\bexport\b/.test(item.kindModifiers)) { - return getSymbolRange(document, item); + return itemSpan; } break; @@ -91,12 +96,12 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens if (item.text === '') { break; } - return getSymbolRange(document, item); + return itemSpan; case PConst.Kind.interface: case PConst.Kind.type: case PConst.Kind.enum: - return getSymbolRange(document, item); + return itemSpan; case PConst.Kind.method: case PConst.Kind.memberGetAccessor: @@ -108,7 +113,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens if (parent && typeConverters.Position.fromLocation(parent.spans[0].start).isEqual(typeConverters.Position.fromLocation(item.spans[0].start)) ) { - return null; + return undefined; } // Only show if parent is a class type object (not a literal) @@ -116,12 +121,12 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.class: case PConst.Kind.interface: case PConst.Kind.type: - return getSymbolRange(document, item); + return itemSpan; } break; } - return null; + return undefined; } } diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index 8cd15f148ea..e505e794279 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Command, CommandManager } from '../commands/commandManager'; import type * as Proto from '../protocol'; +import protocol = require('../protocol'); import * as PConst from '../protocol.const'; import { ClientCapability, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; import API from '../utils/api'; @@ -134,18 +135,7 @@ class MyCompletionItem extends vscode.CompletionItem { this.kind = vscode.CompletionItemKind.Color; } - if (tsEntry.kind === PConst.Kind.script) { - for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) { - if (kindModifiers.has(extModifier)) { - if (tsEntry.name.toLowerCase().endsWith(extModifier)) { - this.detail = tsEntry.name; - } else { - this.detail = tsEntry.name + extModifier; - } - break; - } - } - } + this.detail = getScriptKindDetails(tsEntry); } this.resolveRange(); @@ -206,7 +196,11 @@ class MyCompletionItem extends vscode.CompletionItem { const detail = response.body[0]; - this.detail = this.getDetails(client, detail); + const newItemDetails = this.getDetails(client, detail); + if (newItemDetails) { + this.detail = newItemDetails; + } + this.documentation = this.getDocumentation(client, detail, this.document.uri); const codeAction = this.getCodeActions(detail, filepath); @@ -253,6 +247,11 @@ class MyCompletionItem extends vscode.CompletionItem { ): string | undefined { const parts: string[] = []; + if (detail.kind === PConst.Kind.script) { + // details were already added + return undefined; + } + for (const action of detail.codeActions ?? []) { parts.push(action.description); } @@ -519,6 +518,24 @@ class MyCompletionItem extends vscode.CompletionItem { } } +function getScriptKindDetails(tsEntry: protocol.CompletionEntry,): string | undefined { + if (!tsEntry.kindModifiers || tsEntry.kind !== PConst.Kind.script) { + return; + } + + const kindModifiers = parseKindModifier(tsEntry.kindModifiers); + for (const extModifier of PConst.KindModifiers.fileExtensionKindModifiers) { + if (kindModifiers.has(extModifier)) { + if (tsEntry.name.toLowerCase().endsWith(extModifier)) { + return tsEntry.name; + } else { + return tsEntry.name + extModifier; + } + } + } + return undefined; +} + class CompletionAcceptedCommand implements Command { public static readonly ID = '_typescript.onCompletionAccepted'; diff --git a/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts b/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts index 3a7b43ed5eb..2758b4227b0 100644 --- a/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts +++ b/extensions/typescript-language-features/src/languageFeatures/documentHighlight.ts @@ -6,7 +6,6 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; -import { flatten } from '../utils/arrays'; import { DocumentSelector } from '../utils/documentSelector'; import * as typeConverters from '../utils/typeConverters'; @@ -34,10 +33,10 @@ class TypeScriptDocumentHighlightProvider implements vscode.DocumentHighlightPro return []; } - return flatten( - response.body - .filter(highlight => highlight.file === file) - .map(convertDocumentHighlight)); + return response.body + .filter(highlight => highlight.file === file) + .map(convertDocumentHighlight) + .flat(); } } diff --git a/extensions/typescript-language-features/src/languageFeatures/folding.ts b/extensions/typescript-language-features/src/languageFeatures/folding.ts index 8c90d391ef1..b7c8087d740 100644 --- a/extensions/typescript-language-features/src/languageFeatures/folding.ts +++ b/extensions/typescript-language-features/src/languageFeatures/folding.ts @@ -48,7 +48,7 @@ class TypeScriptFoldingProvider implements vscode.FoldingRangeProvider { // Workaround for #49904 if (span.kind === 'comment') { const line = document.lineAt(range.start.line).text; - if (line.match(/\/\/\s*#endregion/gi)) { + if (/\/\/\s*#endregion/gi.test(line)) { return undefined; } } diff --git a/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts b/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts index 1b7342eba15..10aec536636 100644 --- a/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/semanticTokens.ts @@ -3,17 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// all constants are const import * as vscode from 'vscode'; import * as Proto from '../protocol'; -import { ClientCapability, ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; import { conditionalRegistration, requireMinVersion, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; - -const minTypeScriptVersion = API.fromVersionString(`${VersionRequirement.major}.${VersionRequirement.minor}`); - // as we don't do deltas, for performance reasons, don't compute semantic tokens for documents above that limit const CONTENT_LENGTH_LIMIT = 100000; @@ -22,35 +18,33 @@ export function register( client: ITypeScriptServiceClient, ) { return conditionalRegistration([ - requireMinVersion(client, minTypeScriptVersion), + requireMinVersion(client, API.v370), requireSomeCapability(client, ClientCapability.Semantic), ], () => { const provider = new DocumentSemanticTokensProvider(client); - return vscode.Disposable.from( - // register only as a range provider - vscode.languages.registerDocumentRangeSemanticTokensProvider(selector.semantic, provider, provider.getLegend()), - ); + return vscode.languages.registerDocumentRangeSemanticTokensProvider(selector.semantic, provider, provider.getLegend()); }); } class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensProvider, vscode.DocumentRangeSemanticTokensProvider { - constructor(private readonly client: ITypeScriptServiceClient) { - } + constructor( + private readonly client: ITypeScriptServiceClient + ) { } - getLegend(): vscode.SemanticTokensLegend { + public getLegend(): vscode.SemanticTokensLegend { return new vscode.SemanticTokensLegend(tokenTypes, tokenModifiers); } - async provideDocumentSemanticTokens(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + public async provideDocumentSemanticTokens(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { const file = this.client.toOpenedFilePath(document); if (!file || document.getText().length > CONTENT_LENGTH_LIMIT) { return null; } - return this._provideSemanticTokens(document, { file, start: 0, length: document.getText().length }, token); + return this.provideSemanticTokens(document, { file, start: 0, length: document.getText().length }, token); } - async provideDocumentRangeSemanticTokens(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise { + public async provideDocumentRangeSemanticTokens(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): Promise { const file = this.client.toOpenedFilePath(document); if (!file || (document.offsetAt(range.end) - document.offsetAt(range.start) > CONTENT_LENGTH_LIMIT)) { return null; @@ -58,10 +52,10 @@ class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensPro const start = document.offsetAt(range.start); const length = document.offsetAt(range.end) - start; - return this._provideSemanticTokens(document, { file, start, length }, token); + return this.provideSemanticTokens(document, { file, start, length }, token); } - async _provideSemanticTokens(document: vscode.TextDocument, requestArg: Proto.EncodedSemanticClassificationsRequestArgs, token: vscode.CancellationToken): Promise { + private async provideSemanticTokens(document: vscode.TextDocument, requestArg: Proto.EncodedSemanticClassificationsRequestArgs, token: vscode.CancellationToken): Promise { const file = this.client.toOpenedFilePath(document); if (!file) { return null; @@ -69,9 +63,7 @@ class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensPro const versionBeforeRequest = document.version; - requestArg.format = '2020'; - - const response = await (this.client as ExperimentalProtocol.IExtendedTypeScriptServiceClient).execute('encodedSemanticClassifications-full', requestArg, token, { + const response = await this.client.execute('encodedSemanticClassifications-full', { ...requestArg, format: '2020' }, token, { cancelOnResourceChange: document.uri }); if (response.type !== 'response' || !response.body) { @@ -97,24 +89,18 @@ class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensPro const tokenSpan = response.body.spans; const builder = new vscode.SemanticTokensBuilder(); - let i = 0; - while (i < tokenSpan.length) { + for (let i = 0; i < tokenSpan.length;) { const offset = tokenSpan[i++]; const length = tokenSpan[i++]; const tsClassification = tokenSpan[i++]; - let tokenModifiers = 0; - let tokenType = getTokenTypeFromClassification(tsClassification); - if (tokenType !== undefined) { - tokenModifiers = getTokenModifierFromClassification(tsClassification); - } else { - // an old TypeScript server that uses the original ExperimentalProtocol.ClassificationType's - tokenType = tokenTypeMap[tsClassification]; - if (tokenType === undefined) { - continue; - } + const tokenType = getTokenTypeFromClassification(tsClassification); + if (tokenType === undefined) { + continue; } + const tokenModifiers = getTokenModifierFromClassification(tsClassification); + // we can use the document's range conversion methods because the result is at the same version as the document const startPos = document.positionAt(offset); const endPos = document.positionAt(offset + length); @@ -125,17 +111,18 @@ class DocumentSemanticTokensProvider implements vscode.DocumentSemanticTokensPro builder.push(line, startCharacter, endCharacter - startCharacter, tokenType, tokenModifiers); } } + return builder.build(); } } function waitForDocumentChangesToEnd(document: vscode.TextDocument) { let version = document.version; - return new Promise((s) => { + return new Promise((resolve) => { const iv = setInterval(_ => { if (document.version === version) { clearInterval(iv); - s(); + resolve(); } version = document.version; }, 400); @@ -146,7 +133,7 @@ function waitForDocumentChangesToEnd(document: vscode.TextDocument) { // typescript encodes type and modifiers in the classification: // TSClassification = (TokenType + 1) << 8 + TokenModifier -declare const enum TokenType { +const enum TokenType { class = 0, enum = 1, interface = 2, @@ -161,7 +148,8 @@ declare const enum TokenType { method = 11, _ = 12 } -declare const enum TokenModifier { + +const enum TokenModifier { declaration = 0, static = 1, async = 2, @@ -170,14 +158,11 @@ declare const enum TokenModifier { local = 5, _ = 6 } -declare const enum TokenEncodingConsts { + +const enum TokenEncodingConsts { typeOffset = 8, modifierMask = 255 } -declare const enum VersionRequirement { - major = 3, - minor = 7 -} function getTokenTypeFromClassification(tsClassification: number): number | undefined { if (tsClassification > TokenEncodingConsts.modifierMask) { @@ -211,95 +196,3 @@ tokenModifiers[TokenModifier.readonly] = 'readonly'; tokenModifiers[TokenModifier.static] = 'static'; tokenModifiers[TokenModifier.local] = 'local'; tokenModifiers[TokenModifier.defaultLibrary] = 'defaultLibrary'; - -// mapping for the original ExperimentalProtocol.ClassificationType from TypeScript (only used when plugin is not available) -const tokenTypeMap: number[] = []; -tokenTypeMap[ExperimentalProtocol.ClassificationType.className] = TokenType.class; -tokenTypeMap[ExperimentalProtocol.ClassificationType.enumName] = TokenType.enum; -tokenTypeMap[ExperimentalProtocol.ClassificationType.interfaceName] = TokenType.interface; -tokenTypeMap[ExperimentalProtocol.ClassificationType.moduleName] = TokenType.namespace; -tokenTypeMap[ExperimentalProtocol.ClassificationType.typeParameterName] = TokenType.typeParameter; -tokenTypeMap[ExperimentalProtocol.ClassificationType.typeAliasName] = TokenType.type; -tokenTypeMap[ExperimentalProtocol.ClassificationType.parameterName] = TokenType.parameter; - -namespace ExperimentalProtocol { - - export interface IExtendedTypeScriptServiceClient { - execute( - command: K, - args: ExperimentalProtocol.ExtendedTsServerRequests[K][0], - token: vscode.CancellationToken, - config?: ExecConfig - ): Promise>; - } - - /** - * A request to get encoded semantic classifications for a span in the file - */ - export interface EncodedSemanticClassificationsRequest extends Proto.FileRequest { - arguments: EncodedSemanticClassificationsRequestArgs; - } - - /** - * Arguments for EncodedSemanticClassificationsRequest request. - */ - export interface EncodedSemanticClassificationsRequestArgs extends Proto.FileRequestArgs { - /** - * Start position of the span. - */ - start: number; - /** - * Length of the span. - */ - length: number; - } - - export const enum EndOfLineState { - None, - InMultiLineCommentTrivia, - InSingleQuoteStringLiteral, - InDoubleQuoteStringLiteral, - InTemplateHeadOrNoSubstitutionTemplate, - InTemplateMiddleOrTail, - InTemplateSubstitutionPosition, - } - - export const enum ClassificationType { - comment = 1, - identifier = 2, - keyword = 3, - numericLiteral = 4, - operator = 5, - stringLiteral = 6, - regularExpressionLiteral = 7, - whiteSpace = 8, - text = 9, - punctuation = 10, - className = 11, - enumName = 12, - interfaceName = 13, - moduleName = 14, - typeParameterName = 15, - typeAliasName = 16, - parameterName = 17, - docCommentTagName = 18, - jsxOpenTagName = 19, - jsxCloseTagName = 20, - jsxSelfClosingTagName = 21, - jsxAttribute = 22, - jsxText = 23, - jsxAttributeStringLiteralValue = 24, - bigintLiteral = 25, - } - - export interface EncodedSemanticClassificationsResponse extends Proto.Response { - body?: { - endOfLineState: EndOfLineState; - spans: number[]; - }; - } - - export interface ExtendedTsServerRequests { - 'encodedSemanticClassifications-full': [ExperimentalProtocol.EncodedSemanticClassificationsRequestArgs, ExperimentalProtocol.EncodedSemanticClassificationsResponse]; - } -} diff --git a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts index 4e5eda244b6..b64ead7ca15 100644 --- a/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts +++ b/extensions/typescript-language-features/src/languageFeatures/tsconfig.ts @@ -8,7 +8,7 @@ import { basename, dirname, join, posix } from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { Utils } from 'vscode-uri'; -import { coalesce, flatten } from '../utils/arrays'; +import { coalesce } from '../utils/arrays'; import { exists } from '../utils/fs'; function mapChildren(node: jsonc.Node | undefined, f: (x: jsonc.Node) => R): R[] { @@ -195,9 +195,9 @@ export function register() { const languages = ['json', 'jsonc']; - const selector: vscode.DocumentSelector = flatten( - languages.map(language => - patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern })))); + const selector: vscode.DocumentSelector = + languages.map(language => patterns.map((pattern): vscode.DocumentFilter => ({ language, pattern }))) + .flat(); return vscode.Disposable.from( vscode.commands.registerCommand(openExtendsLinkCommandId, async ({ resourceUri, extendsValue, }: OpenExtendsLinkCommandArgs) => { diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 1a7432674fc..8ad2efac84c 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -11,7 +11,6 @@ import { TsServerProcessFactory } from './tsServer/server'; import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { ActiveJsTsEditorTracker } from './utils/activeJsTsEditorTracker'; -import { flatten } from './utils/arrays'; import { ServiceConfigurationProvider } from './utils/configuration'; import * as fileSchemes from './utils/fileSchemes'; import { standardLanguageDescriptions } from './utils/languageDescription'; @@ -55,10 +54,10 @@ export function lazilyActivateClient( ): vscode.Disposable { const disposables: vscode.Disposable[] = []; - const supportedLanguage = flatten([ + const supportedLanguage = [ ...standardLanguageDescriptions.map(x => x.languageIds), ...pluginManager.plugins.map(x => x.languages) - ]); + ].flat(); let hasActivated = false; const maybeActivate = (textDocument: vscode.TextDocument): boolean => { diff --git a/extensions/typescript-language-features/src/task/taskProvider.ts b/extensions/typescript-language-features/src/task/taskProvider.ts index 75ca51278dd..85484f6d729 100644 --- a/extensions/typescript-language-features/src/task/taskProvider.ts +++ b/extensions/typescript-language-features/src/task/taskProvider.ts @@ -9,7 +9,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { wait } from '../test/testUtils'; import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; -import { coalesce, flatten } from '../utils/arrays'; +import { coalesce } from '../utils/arrays'; import { Disposable } from '../utils/dispose'; import { exists } from '../utils/fs'; import { isTsConfigFileName } from '../utils/languageDescription'; @@ -98,10 +98,10 @@ class TscTaskProvider extends Disposable implements vscode.TaskProvider { } private async getAllTsConfigs(token: vscode.CancellationToken): Promise { - const configs = flatten(await Promise.all([ + const configs = (await Promise.all([ this.getTsConfigForActiveFile(token), this.getTsConfigsInWorkspace(token), - ])); + ])).flat(); return Promise.all( configs.map(async config => await exists(config.uri) ? config : undefined), diff --git a/extensions/typescript-language-features/src/test/smoke/completions.test.ts b/extensions/typescript-language-features/src/test/smoke/completions.test.ts index 6ed0a8b4928..4c0c5f61e5d 100644 --- a/extensions/typescript-language-features/src/test/smoke/completions.test.ts +++ b/extensions/typescript-language-features/src/test/smoke/completions.test.ts @@ -14,7 +14,7 @@ const testDocumentUri = vscode.Uri.parse('untitled:test.ts'); const insertModes = Object.freeze(['insert', 'replace']); suite.skip('TypeScript Completions', () => { - const configDefaults: VsCodeConfiguration = Object.freeze({ + const configDefaults = Object.freeze({ [Config.autoClosingBrackets]: 'always', [Config.typescriptCompleteFunctionCalls]: false, [Config.insertMode]: 'insert', diff --git a/extensions/typescript-language-features/src/test/smoke/jsDocCompletions.test.ts b/extensions/typescript-language-features/src/test/smoke/jsDocCompletions.test.ts index a347c5b67d0..4c4cf963801 100644 --- a/extensions/typescript-language-features/src/test/smoke/jsDocCompletions.test.ts +++ b/extensions/typescript-language-features/src/test/smoke/jsDocCompletions.test.ts @@ -14,7 +14,7 @@ const testDocumentUri = vscode.Uri.parse('untitled:test.ts'); suite('JSDoc Completions', () => { const _disposables: vscode.Disposable[] = []; - const configDefaults: VsCodeConfiguration = Object.freeze({ + const configDefaults = Object.freeze({ [Config.snippetSuggestions]: 'inline', }); diff --git a/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts b/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts index f4bed53bb22..f6259e0d276 100644 --- a/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts +++ b/extensions/typescript-language-features/src/test/smoke/referencesCodeLens.test.ts @@ -29,7 +29,7 @@ namespace Config { } suite('TypeScript References', () => { - const configDefaults: VsCodeConfiguration = Object.freeze({ + const configDefaults = Object.freeze({ [Config.referencesCodeLens]: true, }); diff --git a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts index 0543a6ae847..c8635b2f668 100644 --- a/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts +++ b/extensions/typescript-language-features/src/typeScriptServiceClientHost.ts @@ -23,7 +23,7 @@ import TypeScriptServiceClient from './typescriptServiceClient'; import { IntellisenseStatus } from './ui/intellisenseStatus'; import { VersionStatus } from './ui/versionStatus'; import { ActiveJsTsEditorTracker } from './utils/activeJsTsEditorTracker'; -import { coalesce, flatten } from './utils/arrays'; +import { coalesce } from './utils/arrays'; import { ServiceConfigurationProvider } from './utils/configuration'; import { Disposable } from './utils/dispose'; import * as errorCodes from './utils/errorCodes'; @@ -165,11 +165,10 @@ export default class TypeScriptServiceClientHost extends Disposable { } private getAllModeIds(descriptions: LanguageDescription[], pluginManager: PluginManager) { - const allModeIds = flatten([ + return [ ...descriptions.map(x => x.languageIds), ...pluginManager.plugins.map(x => x.languages) - ]); - return allModeIds; + ].flat(); } public get serviceClient(): TypeScriptServiceClient { diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index 5afaac77ecd..616131b26d8 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -30,7 +30,9 @@ export namespace ServerResponse { export const NoContent = { type: 'noContent' } as const; - export type Response = T | Cancelled | typeof NoContent; + export const NoServer = { type: 'noServer' } as const; + + export type Response = T | Cancelled | typeof NoContent | typeof NoServer; } interface StandardTsServerRequests { @@ -70,6 +72,7 @@ interface StandardTsServerRequests { 'provideCallHierarchyOutgoingCalls': [Proto.FileLocationRequestArgs, Proto.ProvideCallHierarchyOutgoingCallsResponse]; 'fileReferences': [Proto.FileRequestArgs, Proto.FileReferencesResponse]; 'provideInlayHints': [Proto.InlayHintsRequestArgs, Proto.InlayHintsResponse]; + 'encodedSemanticClassifications-full': [Proto.EncodedSemanticClassificationsRequestArgs, Proto.EncodedSemanticClassificationsResponse]; } interface NoResponseTsServerRequests { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 937738e81d5..172a61301fd 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -280,13 +280,20 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.loadingIndicator.reset(); } - public restartTsServer(): void { + public restartTsServer(fromUserAction = false): void { if (this.serverState.type === ServerState.Type.Running) { this.info('Killing TS Server'); this.isRestarting = true; this.serverState.server.kill(); } + if (fromUserAction) { + // Reset crash trackers + this.hasServerFatallyCrashedTooManyTimes = false; + this.numberRestarts = 0; + this.lastStart = Date.now(); + } + this.serverState = this.startService(true); } @@ -340,20 +347,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.telemetryReporter.logTelemetry(eventName, properties); } - private service(): ServerState.Running { - if (this.serverState.type === ServerState.Type.Running) { - return this.serverState; - } - if (this.serverState.type === ServerState.Type.Errored) { - throw this.serverState.error; - } - const newState = this.startService(); - if (newState.type === ServerState.Type.Running) { - return newState; - } - throw new Error(`Could not create TS service. Service state:${JSON.stringify(newState)}`); - } - public ensureServiceStarted() { if (this.serverState.type !== ServerState.Type.Running) { this.startService(); @@ -731,7 +724,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType if (filepath.startsWith(this.inMemoryResourcePrefix)) { const parts = filepath.match(/^\^\/([^\/]+)\/(.+)$/); if (parts) { - const resource = vscode.Uri.parse(parts[1] + ':' + parts[2]); + const resource = vscode.Uri.parse(parts[1] + ':/' + parts[2]); return this.bufferSyncSupport.toVsCodeResource(resource); } } @@ -767,31 +760,34 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise> { - let executions: Array> | undefined>; + let executions: Array> | undefined> | undefined; if (config?.cancelOnResourceChange) { - const runningServerState = this.service(); + const runningServerState = this.serverState; + if (runningServerState.type === ServerState.Type.Running) { + const source = new vscode.CancellationTokenSource(); + token.onCancellationRequested(() => source.cancel()); - const source = new vscode.CancellationTokenSource(); - token.onCancellationRequested(() => source.cancel()); + const inFlight: ToCancelOnResourceChanged = { + resource: config.cancelOnResourceChange, + cancel: () => source.cancel(), + }; + runningServerState.toCancelOnResourceChange.add(inFlight); - const inFlight: ToCancelOnResourceChanged = { - resource: config.cancelOnResourceChange, - cancel: () => source.cancel(), - }; - runningServerState.toCancelOnResourceChange.add(inFlight); + executions = this.executeImpl(command, args, { + isAsync: false, + token: source.token, + expectsResult: true, + ...config, + }); + executions[0]!.finally(() => { + runningServerState.toCancelOnResourceChange.delete(inFlight); + source.dispose(); + }); + } + } - executions = this.executeImpl(command, args, { - isAsync: false, - token: source.token, - expectsResult: true, - ...config, - }); - executions[0]!.finally(() => { - runningServerState.toCancelOnResourceChange.delete(inFlight); - source.dispose(); - }); - } else { + if (!executions) { executions = this.executeImpl(command, args, { isAsync: false, token, @@ -831,9 +827,13 @@ export default class TypeScriptServiceClient extends Disposable implements IType } private executeImpl(command: keyof TypeScriptRequests, args: any, executeInfo: { isAsync: boolean; token?: vscode.CancellationToken; expectsResult: boolean; lowPriority?: boolean; requireSemantic?: boolean }): Array> | undefined> { - this.bufferSyncSupport.beforeCommand(command); - const runningServerState = this.service(); - return runningServerState.server.executeImpl(command, args, executeInfo); + const serverState = this.serverState; + if (serverState.type === ServerState.Type.Running) { + this.bufferSyncSupport.beforeCommand(command); + return serverState.server.executeImpl(command, args, executeInfo); + } else { + return [Promise.resolve(ServerResponse.NoServer)]; + } } public interruptGetErr(f: () => R): R { diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index 56e98f18024..e4c6133cc15 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -29,6 +29,7 @@ export default class API { public static readonly v340 = API.fromSimpleString('3.4.0'); public static readonly v345 = API.fromSimpleString('3.4.5'); public static readonly v350 = API.fromSimpleString('3.5.0'); + public static readonly v370 = API.fromSimpleString('3.7.0'); public static readonly v380 = API.fromSimpleString('3.8.0'); public static readonly v381 = API.fromSimpleString('3.8.1'); public static readonly v390 = API.fromSimpleString('3.9.0'); diff --git a/extensions/typescript-language-features/src/utils/arrays.ts b/extensions/typescript-language-features/src/utils/arrays.ts index 3619d828dc3..a25bbe54733 100644 --- a/extensions/typescript-language-features/src/utils/arrays.ts +++ b/extensions/typescript-language-features/src/utils/arrays.ts @@ -19,10 +19,6 @@ export function equals( return a.every((x, i) => itemEquals(x, b[i])); } -export function flatten(array: ReadonlyArray[]): T[] { - return Array.prototype.concat.apply([], array); -} - export function coalesce(array: ReadonlyArray): T[] { return array.filter(e => !!e); } diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 1249cd5b6ba..c69b8fde758 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -52,12 +52,16 @@ export const enum SyntaxServerConfiguration { export class ImplicitProjectConfiguration { + public readonly target: string | undefined; + public readonly module: string | undefined; public readonly checkJs: boolean; public readonly experimentalDecorators: boolean; public readonly strictNullChecks: boolean; public readonly strictFunctionTypes: boolean; constructor(configuration: vscode.WorkspaceConfiguration) { + this.target = ImplicitProjectConfiguration.readTarget(configuration); + this.module = ImplicitProjectConfiguration.readModule(configuration); this.checkJs = ImplicitProjectConfiguration.readCheckJs(configuration); this.experimentalDecorators = ImplicitProjectConfiguration.readExperimentalDecorators(configuration); this.strictNullChecks = ImplicitProjectConfiguration.readImplicitStrictNullChecks(configuration); @@ -68,6 +72,14 @@ export class ImplicitProjectConfiguration { return objects.equals(this, other); } + private static readTarget(configuration: vscode.WorkspaceConfiguration): string | undefined { + return configuration.get('js/ts.implicitProjectConfig.target'); + } + + private static readModule(configuration: vscode.WorkspaceConfiguration): string | undefined { + return configuration.get('js/ts.implicitProjectConfig.module'); + } + private static readCheckJs(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('js/ts.implicitProjectConfig.checkJs') ?? configuration.get('javascript.implicitProjectConfig.checkJs', false); diff --git a/extensions/typescript-language-features/src/utils/plugins.ts b/extensions/typescript-language-features/src/utils/plugins.ts index 0716aba36cb..92d61e3e235 100644 --- a/extensions/typescript-language-features/src/utils/plugins.ts +++ b/extensions/typescript-language-features/src/utils/plugins.ts @@ -37,7 +37,7 @@ export class PluginManager extends Disposable { return; } const newPlugins = this.readPlugins(); - if (!arrays.equals(arrays.flatten(Array.from(this._plugins.values())), arrays.flatten(Array.from(newPlugins.values())), TypeScriptServerPlugin.equals)) { + if (!arrays.equals(Array.from(this._plugins.values()).flat(), Array.from(newPlugins.values()).flat(), TypeScriptServerPlugin.equals)) { this._plugins = newPlugins; this._onDidUpdatePlugins.fire(this); } @@ -48,7 +48,7 @@ export class PluginManager extends Disposable { if (!this._plugins) { this._plugins = this.readPlugins(); } - return arrays.flatten(Array.from(this._plugins.values())); + return Array.from(this._plugins.values()).flat(); } private readonly _onDidUpdatePlugins = this._register(new vscode.EventEmitter()); diff --git a/extensions/typescript-language-features/src/utils/previewer.ts b/extensions/typescript-language-features/src/utils/previewer.ts index 9d685dcc2fe..a4a42086d17 100644 --- a/extensions/typescript-language-features/src/utils/previewer.ts +++ b/extensions/typescript-language-features/src/utils/previewer.ts @@ -41,7 +41,7 @@ function getTagBodyText( // Convert to markdown code block if it does not already contain one function makeCodeblock(text: string): string { - if (text.match(/^\s*[~`]{3}/m)) { + if (/^\s*[~`]{3}/m.test(text)) { return text; } return '```\n' + text + '\n```'; diff --git a/extensions/typescript-language-features/src/utils/tsconfig.ts b/extensions/typescript-language-features/src/utils/tsconfig.ts index d72d7489e33..b92a26916d1 100644 --- a/extensions/typescript-language-features/src/utils/tsconfig.ts +++ b/extensions/typescript-language-features/src/utils/tsconfig.ts @@ -22,15 +22,18 @@ export function isImplicitProjectConfigFile(configFileName: string) { return configFileName.startsWith('/dev/null/'); } +const defaultProjectConfig = Object.freeze({ + module: 'ESNext' as Proto.ModuleKind, + moduleResolution: 'Node' as Proto.ModuleResolutionKind, + target: 'ES2020' as Proto.ScriptTarget, + jsx: 'preserve' as Proto.JsxEmit, +}); + export function inferredProjectCompilerOptions( projectType: ProjectType, serviceConfig: TypeScriptServiceConfiguration, ): Proto.ExternalProjectCompilerOptions { - const projectConfig: Proto.ExternalProjectCompilerOptions = { - module: 'commonjs' as Proto.ModuleKind, - target: 'es2020' as Proto.ScriptTarget, - jsx: 'preserve' as Proto.JsxEmit, - }; + const projectConfig = { ...defaultProjectConfig }; if (serviceConfig.implicitProjectConfiguration.checkJs) { projectConfig.checkJs = true; @@ -51,6 +54,15 @@ export function inferredProjectCompilerOptions( projectConfig.strictFunctionTypes = true; } + + if (serviceConfig.implicitProjectConfiguration.module) { + projectConfig.module = serviceConfig.implicitProjectConfiguration.module as Proto.ModuleKind; + } + + if (serviceConfig.implicitProjectConfiguration.target) { + projectConfig.target = serviceConfig.implicitProjectConfiguration.target as Proto.ScriptTarget; + } + if (projectType === ProjectType.TypeScript) { projectConfig.sourceMap = true; } diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 883b4b81749..07d4066f89b 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -12,7 +12,6 @@ "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.inlayHints.d.ts", "../../src/vscode-dts/vscode.proposed.languageStatus.d.ts", - "../../src/vscode-dts/vscode.proposed.markdownStringBaseUri.d.ts", "../../src/vscode-dts/vscode.proposed.resolvers.d.ts", "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", ] diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index 40d17f3173a..2292b38d296 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -2,20 +2,20 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== -"@vscode/extension-telemetry@0.4.6": - version "0.4.6" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.6.tgz#2f4c5bf81adf6b2e4ddba54759355e1559c5476b" - integrity sha512-bDXwHoNXIR1Rc8xdphJ4B3rWdzAGm+FUPk4mJl6/oyZmfEX+QdlDLxnCwlv/vxHU1p11ThHSB8kRhsWZ1CzOqw== +"@vscode/extension-telemetry@0.4.10": + version "0.4.10" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" + integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== jsonc-parser@^2.2.1: version "2.3.1" diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 9ea109c5afc..abe4aff2dc3 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -17,7 +17,6 @@ "findTextInFiles", "fsChunks", "inlineCompletions", - "markdownStringBaseUri", "notebookCellExecutionState", "notebookConcatTextDocument", "notebookContentProvider", @@ -47,7 +46,6 @@ "textSearchProvider", "timeline", "tokenInformation", - "treeViewDragAndDrop", "treeViewReveal", "workspaceTrust", "telemetry" @@ -201,7 +199,7 @@ }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "14.x" + "@types/node": "16.x" }, "repository": { "type": "git", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts index 780ed8ddc1e..e2f5619e063 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts @@ -112,15 +112,15 @@ suite('vscode API - commands', () => { await commands.executeCommand('vscode.open', uri); assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.One); - assert.strictEqual(window.tabGroups.all[0].activeTab?.viewColumn, ViewColumn.One); + assert.strictEqual(window.tabGroups.groups[0].activeTab?.parentGroup.viewColumn, ViewColumn.One); await commands.executeCommand('vscode.open', uri, ViewColumn.Two); assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.Two); - assert.strictEqual(window.tabGroups.all[1].activeTab?.viewColumn, ViewColumn.Two); + assert.strictEqual(window.tabGroups.groups[1].activeTab?.parentGroup.viewColumn, ViewColumn.Two); await commands.executeCommand('vscode.open', uri, ViewColumn.One); assert.strictEqual(window.activeTextEditor?.viewColumn, ViewColumn.One); - assert.strictEqual(window.tabGroups.all[0].activeTab?.viewColumn, ViewColumn.One); + assert.strictEqual(window.tabGroups.groups[0].activeTab?.parentGroup.viewColumn, ViewColumn.One); let e1: Error | undefined = undefined; try { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index 5c5d0c88143..2e3a32cd9ce 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -902,33 +902,6 @@ const apiTestContentProvider: vscode.NotebookContentProvider = { assert.strictEqual(executionWasCancelled, true); }); - - test('execution cancelled when kernel changed', async () => { - await openRandomNotebookDocument(); - let executionWasCancelled = false; - const cancelledKernel = new class extends Kernel { - constructor() { - super('cancelledKernel', ''); - } - - override async _execute(cells: vscode.NotebookCell[]) { - const [cell] = cells; - const exe = this.controller.createNotebookCellExecution(cell); - exe.token.onCancellationRequested(() => executionWasCancelled = true); - } - }; - - const notebook = await openRandomNotebookDocument(); - await vscode.window.showNotebookDocument(notebook); - testDisposables.push(cancelledKernel.controller); - await assertKernel(cancelledKernel, notebook); - await vscode.commands.executeCommand('notebook.cell.execute'); - - const newKernel = new Kernel('newKernel', 'kernel'); - testDisposables.push(newKernel.controller); - await assertKernel(newKernel, notebook); - assert.strictEqual(executionWasCancelled, true); - }); }); (vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('statusbar', () => { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 3c1d22d5202..e58f295a475 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { join } from 'path'; -import { CancellationTokenSource, commands, MarkdownString, Position, QuickPickItem, Selection, StatusBarAlignment, TextEditor, TextEditorSelectionChangeKind, TextEditorViewColumnChangeEvent, Uri, ViewColumn, window, workspace } from 'vscode'; +import { CancellationTokenSource, commands, MarkdownString, NotebookEditorTabInput, Position, QuickPickItem, Selection, StatusBarAlignment, TextDiffTabInput, TextEditor, TextEditorSelectionChangeKind, TextEditorViewColumnChangeEvent, TextTabInput, Uri, ViewColumn, window, workspace } from 'vscode'; import { assertNoRpc, closeAllEditors, createRandomFile, pathEquals } from '../utils'; @@ -383,21 +383,22 @@ suite('vscode API - window', () => { await window.showTextDocument(docC, { viewColumn: ViewColumn.Two, preview: false }); const tabGroups = window.tabGroups; - assert.strictEqual(tabGroups.all.length, 2); + assert.strictEqual(tabGroups.groups.length, 2); - const group1Tabs = tabGroups.all[0].tabs; + const group1Tabs = tabGroups.groups[0].tabs; assert.strictEqual(group1Tabs.length, 2); - const group2Tabs = tabGroups.all[1].tabs; + const group2Tabs = tabGroups.groups[1].tabs; assert.strictEqual(group2Tabs.length, 1); - await group1Tabs[0].move(1, ViewColumn.One); + await tabGroups.move(group1Tabs[0], ViewColumn.One, 1); }); + // TODO @lramos15 re-enable these once shape is more stable test('Tabs - vscode.open & vscode.diff', async function () { // Simple function to get the active tab const getActiveTab = () => { - return window.tabGroups.all.find(g => g.isActive)?.activeTab; + return window.tabGroups.groups.find(g => g.isActive)?.activeTab; }; const [docA, docB, docC] = await Promise.all([ @@ -413,20 +414,23 @@ suite('vscode API - window', () => { const commandFile = await createRandomFile(); await commands.executeCommand('vscode.open', commandFile, ViewColumn.Three); // Ensure active tab is correct after calling vscode.opn - assert.strictEqual(getActiveTab()?.viewColumn, ViewColumn.Three); + assert.strictEqual(getActiveTab()?.parentGroup.viewColumn, ViewColumn.Three); const leftDiff = await createRandomFile(); const rightDiff = await createRandomFile(); await commands.executeCommand('vscode.diff', leftDiff, rightDiff, 'Diff', { viewColumn: ViewColumn.Four, preview: false }); - assert.strictEqual(getActiveTab()?.viewColumn, ViewColumn.Four); + assert.strictEqual(getActiveTab()?.parentGroup.viewColumn, ViewColumn.Four); - const tabs = window.tabGroups.all.map(g => g.tabs).flat(1); + const tabs = window.tabGroups.groups.map(g => g.tabs).flat(1); assert.strictEqual(tabs.length, 5); - assert.strictEqual(tabs[0].resource?.toString(), docA.uri.toString()); - assert.strictEqual(tabs[1].resource?.toString(), docB.uri.toString()); - assert.strictEqual(tabs[2].resource?.toString(), docC.uri.toString()); - assert.strictEqual(tabs[3].resource?.toString(), commandFile.toString()); - + assert.ok(tabs[0].input instanceof TextTabInput); + assert.strictEqual(tabs[0].input.uri.toString(), docA.uri.toString()); + assert.ok(tabs[1].input instanceof TextTabInput); + assert.strictEqual(tabs[1].input.uri.toString(), docB.uri.toString()); + assert.ok(tabs[2].input instanceof TextTabInput); + assert.strictEqual(tabs[2].input.uri.toString(), docC.uri.toString()); + assert.ok(tabs[3].input instanceof TextTabInput); + assert.strictEqual(tabs[3].input.uri.toString(), commandFile.toString()); }); test('Tabs - Ensure tabs getter is correct', async function () { @@ -451,25 +455,29 @@ suite('vscode API - window', () => { const rightDiff = await createRandomFile(); await commands.executeCommand('vscode.diff', leftDiff, rightDiff, 'Diff', { viewColumn: ViewColumn.Three, preview: false }); - const tabs = window.tabGroups.all.map(g => g.tabs).flat(1); + const tabs = window.tabGroups.groups.map(g => g.tabs).flat(1); assert.strictEqual(tabs.length, 5); // All resources should match the text documents as they're the only tabs currently open - assert.strictEqual(tabs[0].resource?.toString(), docA.uri.toString()); - assert.strictEqual(tabs[1].resource?.toString(), notebookDoc.uri.toString()); - assert.strictEqual(tabs[2].resource?.toString(), docB.uri.toString()); - assert.strictEqual(tabs[3].resource?.toString(), docC.uri.toString()); + assert.ok(tabs[0].input instanceof TextTabInput); + assert.strictEqual(tabs[0].input.uri.toString(), docA.uri.toString()); + assert.ok(tabs[1].input instanceof NotebookEditorTabInput); + assert.strictEqual(tabs[1].input.uri.toString(), notebookDoc.uri.toString()); + assert.ok(tabs[2].input instanceof TextTabInput); + assert.strictEqual(tabs[2].input.uri.toString(), docB.uri.toString()); + assert.ok(tabs[3].input instanceof TextTabInput); + assert.strictEqual(tabs[3].input.uri.toString(), docC.uri.toString()); // Diff editor and side by side editor report the right side as the resource - assert.strictEqual(tabs[4].resource?.toString(), rightDiff.toString()); + assert.ok(tabs[4].input instanceof TextDiffTabInput); + assert.strictEqual(tabs[4].input.modified.toString(), rightDiff.toString()); - assert.strictEqual(tabs[0].viewColumn, ViewColumn.One); - assert.strictEqual(tabs[1].viewColumn, ViewColumn.One); - assert.strictEqual(tabs[2].viewColumn, ViewColumn.Two); - assert.strictEqual(tabs[3].viewColumn, ViewColumn.Three); - assert.strictEqual(tabs[4].viewColumn, ViewColumn.Three); + assert.strictEqual(tabs[0].parentGroup.viewColumn, ViewColumn.One); + assert.strictEqual(tabs[1].parentGroup.viewColumn, ViewColumn.One); + assert.strictEqual(tabs[2].parentGroup.viewColumn, ViewColumn.Two); + assert.strictEqual(tabs[3].parentGroup.viewColumn, ViewColumn.Three); + assert.strictEqual(tabs[4].parentGroup.viewColumn, ViewColumn.Three); }); - /* test('Tabs - ensure active tab is correct', async () => { const [docA, docB, docC] = await Promise.all([ @@ -480,21 +488,27 @@ suite('vscode API - window', () => { // Function to acquire the active tab within the active group const getActiveTabInActiveGroup = () => { - const activeGroup = window.tabGroups.all.filter(group => group.isActive)[0]; - return activeGroup.activeTab; + const activeGroup = window.tabGroups.groups.filter(group => group.isActive)[0]; + return activeGroup?.activeTab; }; await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false }); - assert.ok(getActiveTabInActiveGroup()); - assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docA.uri.toString()); + let activeTab = getActiveTabInActiveGroup(); + assert.ok(activeTab); + assert.ok(activeTab.input instanceof TextTabInput); + assert.strictEqual(activeTab.input.uri.toString(), docA.uri.toString()); await window.showTextDocument(docB, { viewColumn: ViewColumn.Two, preview: false }); - assert.ok(getActiveTabInActiveGroup()); - assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docB.uri.toString()); + activeTab = getActiveTabInActiveGroup(); + assert.ok(activeTab); + assert.ok(activeTab.input instanceof TextTabInput); + assert.strictEqual(activeTab.input.uri.toString(), docB.uri.toString()); await window.showTextDocument(docC, { viewColumn: ViewColumn.Three, preview: false }); - assert.ok(getActiveTabInActiveGroup()); - assert.strictEqual(getActiveTabInActiveGroup()?.resource?.toString(), docC.uri.toString()); + activeTab = getActiveTabInActiveGroup(); + assert.ok(activeTab); + assert.ok(activeTab.input instanceof TextTabInput); + assert.strictEqual(activeTab.input.uri.toString(), docC.uri.toString()); await commands.executeCommand('workbench.action.closeActiveEditor'); await commands.executeCommand('workbench.action.closeActiveEditor'); @@ -503,6 +517,8 @@ suite('vscode API - window', () => { assert.ok(!getActiveTabInActiveGroup()); }); + /* + test('Tabs - Move Tab', async () => { const [docA, docB, docC] = await Promise.all([ workspace.openTextDocument(await createRandomFile()), diff --git a/extensions/vscode-api-tests/yarn.lock b/extensions/vscode-api-tests/yarn.lock index b54d6b48836..13c5c61dbd9 100644 --- a/extensions/vscode-api-tests/yarn.lock +++ b/extensions/vscode-api-tests/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index b27439bf8b6..e2517c811f2 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -20,7 +20,7 @@ "jsonc-parser": "2.2.1" }, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "contributes": { "semanticTokenTypes": [ diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.rst b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.rst new file mode 100644 index 00000000000..749bf14f800 --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.rst @@ -0,0 +1,98 @@ +*italics*, **bold**, ``literal``. + +1. A list +2. With items + - With sub-lists ... + - ... of things. +3. Other things + +definition list + A list of terms and their definition + +Literal block:: + + x = 2 + 3 + + +Section separators are all interchangeable. + +===== +Title +===== + +-------- +Subtitle +-------- + +Section 1 +========= + +Section 2 +--------- + +Section 3 +~~~~~~~~~ + +| Keeping line +| breaks. + + ++-------------+--------------+ +| Fancy table | with columns | ++=============+==============+ +| row 1, col 1| row 1, col 2 | ++-------------+--------------+ + +============ ============ +Simple table with columns +============ ============ +row 1, col1 row 1, col 2 +============ ============ + +Block quote is indented. + + This space intentionally not important. + +Doctest block + +>>> 2 +3 +5 + +A footnote [#note]_. + +.. [#note] https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#footnotes + + +Citation [cite]_. + +.. [cite] https://bing.com + +a simple link_. + +A `fancier link`_ . + +.. _link: https://docutils.sourceforge.io/ +.. _fancier link: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html + + +An `inline link `__ . + +.. image:: https://code.visualstudio.com/assets/images/code-stable.png + +.. function: example() + :module: mod + + +:sub:`subscript` +:sup:`superscript` + +.. This is a comment. + +.. + And a bigger, + longer comment. + + +A |subst| of something. + +.. |subst| replace:: substitution diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-brackets_tsx.json b/extensions/vscode-colorize-tests/test/colorize-results/test-brackets_tsx.json index da7d035772e..c5cbb50375c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-brackets_tsx.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-brackets_tsx.json @@ -73,14 +73,14 @@ }, { "c": "Array", - "t": "source.tsx meta.var.expr.tsx meta.function-call.tsx support.class.builtin.tsx", + "t": "source.tsx meta.var.expr.tsx meta.function-call.tsx entity.name.function.tsx", "r": { - "dark_plus": "support.class: #4EC9B0", - "light_plus": "support.class: #267F99", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.class: #4EC9B0", - "hc_light": "support.class: #267F99" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json index 3a97e50dbea..fcd165a649a 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json @@ -181,14 +181,14 @@ }, { "c": "RegExp", - "t": "source.ts meta.var.expr.ts new.expr.ts meta.function-call.ts support.class.builtin.ts", + "t": "source.ts meta.var.expr.ts new.expr.ts meta.function-call.ts entity.name.function.ts", "r": { - "dark_plus": "support.class: #4EC9B0", - "light_plus": "support.class: #267F99", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.class: #4EC9B0", - "hc_light": "support.class: #267F99" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_html.json b/extensions/vscode-colorize-tests/test/colorize-results/test_html.json index 57b19882170..9a832a73d1b 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_html.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_html.json @@ -2089,14 +2089,14 @@ }, { "c": "require", - "t": "text.html.derivative meta.embedded.block.html source.js meta.function-call.js support.function.js", + "t": "text.html.derivative meta.embedded.block.html source.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "meta.embedded: #D4D4D4", "light_vs": "meta.embedded: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_js.json b/extensions/vscode-colorize-tests/test/colorize-results/test_js.json index 39bff0663bb..9e7d46435c5 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_js.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_js.json @@ -145,14 +145,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -301,14 +301,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -457,14 +457,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -613,14 +613,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -769,14 +769,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -1117,14 +1117,14 @@ }, { "c": "require", - "t": "source.js meta.var.expr.js meta.function-call.js support.function.js", + "t": "source.js meta.var.expr.js meta.function-call.js entity.name.function.js", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json b/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json index 239ef361b3c..9633af1a5e6 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json @@ -409,14 +409,14 @@ }, { "c": "fact", - "t": "source.lua entity.name.function.lua", + "t": "source.lua support.function.any-method.lua", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #795E26" + "hc_black": "support.function: #DCDCAA", + "hc_light": "support.function: #795E26" } }, { @@ -805,14 +805,14 @@ }, { "c": "fact", - "t": "source.lua entity.name.function.lua", + "t": "source.lua support.function.any-method.lua", "r": { - "dark_plus": "entity.name.function: #DCDCAA", - "light_plus": "entity.name.function: #795E26", + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "entity.name.function: #DCDCAA", - "hc_light": "entity.name.function: #795E26" + "hc_black": "support.function: #DCDCAA", + "hc_light": "support.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json new file mode 100644 index 00000000000..0cd405f260f --- /dev/null +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json @@ -0,0 +1,1298 @@ +[ + { + "c": "*italics*", + "t": "source.rst markup.italic", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ", ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "**bold**", + "t": "source.rst markup.bold", + "r": { + "dark_plus": "markup.bold: #569CD6", + "light_plus": "markup.bold: #000080", + "dark_vs": "markup.bold: #569CD6", + "light_vs": "markup.bold: #000080", + "hc_black": "default: #FFFFFF", + "hc_light": "markup.bold: #000080" + } + }, + { + "c": ", ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "``literal``", + "t": "source.rst string.interpolated", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "hc_light": "string: #A31515" + } + }, + { + "c": ".", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "1. ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "A list", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "2. ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "With items", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": " - ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "With sub-lists ...", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": " - ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "... of things.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "3. ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "Other things", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "definition list", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": " A list of terms and their definition", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "Literal block", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "::", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " x = 2 + 3", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "Section separators are all interchangeable.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "=====", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "Title", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "=====", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "--------", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "Subtitle", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "--------", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "Section 1", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "=========", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "Section 2", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "---------", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "Section 3", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "~~~~~~~~~", + "t": "source.rst markup.heading", + "r": { + "dark_plus": "markup.heading: #569CD6", + "light_plus": "markup.heading: #800000", + "dark_vs": "markup.heading: #569CD6", + "light_vs": "markup.heading: #800000", + "hc_black": "markup.heading: #6796E6", + "hc_light": "markup.heading: #800000" + } + }, + { + "c": "| ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "Keeping line", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "| ", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "breaks.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "+-------------+--------------+", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " Fancy table ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " with columns ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "+=============+==============+", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " row 1, col 1", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " row 1, col 2 ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "+-------------+--------------+", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "============ ============", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "Simple table with columns", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "============ ============", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "row 1, col1 row 1, col 2", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "============ ============", + "t": "source.rst keyword.control.table", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "Block quote is indented.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": " This space intentionally not important.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "Doctest block", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ">>>", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "2", + "t": "source.rst constant.numeric.dec.python", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #098658" + } + }, + { + "c": " ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "+", + "t": "source.rst keyword.operator.arithmetic.python", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000" + } + }, + { + "c": "3", + "t": "source.rst constant.numeric.dec.python", + "r": { + "dark_plus": "constant.numeric: #B5CEA8", + "light_plus": "constant.numeric: #098658", + "dark_vs": "constant.numeric: #B5CEA8", + "light_vs": "constant.numeric: #098658", + "hc_black": "constant.numeric: #B5CEA8", + "hc_light": "constant.numeric: #098658" + } + }, + { + "c": "5", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "A footnote [#note]_.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ".. [#note] ", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": "https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#footnotes", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "Citation ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "[cite]_", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": ".", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ".. [cite] ", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": "https://bing.com", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "a simple ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "link_", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": ".", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "A ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "`fancier link`_", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": " .", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ".. _link: ", + "t": "source.rst entity.name.tag.anchor", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": "https://docutils.sourceforge.io/", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ".. _fancier link: ", + "t": "source.rst entity.name.tag.anchor", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": "https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "An ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "`inline link `__", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": " .", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": ".. image::", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " https://code.visualstudio.com/assets/images/code-stable.png", + "t": "source.rst variable", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "variable: #9CDCFE", + "hc_light": "variable: #001080" + } + }, + { + "c": ".. function: example()", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": " :module: mod", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": ":sub:", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "`subscript`", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": ":sup:", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": "`superscript`", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": ".. This is a comment.", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": "..", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": " And a bigger,", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": " longer comment.", + "t": "source.rst comment.block", + "r": { + "dark_plus": "comment: #6A9955", + "light_plus": "comment: #008000", + "dark_vs": "comment: #6A9955", + "light_vs": "comment: #008000", + "hc_black": "comment: #7CA668", + "hc_light": "comment: #008000" + } + }, + { + "c": "A ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|subst|", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": " of something.", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "..", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "|subst|", + "t": "source.rst entity.name.tag", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6", + "hc_light": "entity.name.tag: #800000" + } + }, + { + "c": " ", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "replace::", + "t": "source.rst keyword.control", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " substitution", + "t": "source.rst", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + } +] \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json index db5b19bc410..dd92007194e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json @@ -3745,14 +3745,14 @@ }, { "c": "Math", - "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts meta.function-call.ts support.constant.math.ts", + "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts meta.function-call.ts variable.other.object.ts", "r": { - "dark_plus": "support.constant.math: #4EC9B0", - "light_plus": "support.constant.math: #267F99", + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.constant.math: #4EC9B0", - "hc_light": "support.constant.math: #267F99" + "hc_black": "variable: #9CDCFE", + "hc_light": "variable: #001080" } }, { @@ -3769,14 +3769,14 @@ }, { "c": "random", - "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts meta.function-call.ts support.function.math.ts", + "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts meta.function-call.ts entity.name.function.ts", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { @@ -4909,14 +4909,14 @@ }, { "c": "setTimeout", - "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.function-call.ts support.function.ts", + "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.function-call.ts entity.name.function.ts", "r": { - "dark_plus": "support.function: #DCDCAA", - "light_plus": "support.function: #795E26", + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", - "hc_black": "support.function: #DCDCAA", - "hc_light": "support.function: #795E26" + "hc_black": "entity.name.function: #DCDCAA", + "hc_light": "entity.name.function: #795E26" } }, { diff --git a/extensions/vscode-colorize-tests/yarn.lock b/extensions/vscode-colorize-tests/yarn.lock index 15725cff502..44d4327771e 100644 --- a/extensions/vscode-colorize-tests/yarn.lock +++ b/extensions/vscode-colorize-tests/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== jsonc-parser@2.2.1: version "2.2.1" diff --git a/extensions/vscode-custom-editor-tests/package.json b/extensions/vscode-custom-editor-tests/package.json index 778f5c56cf0..1a17bd5d6cb 100644 --- a/extensions/vscode-custom-editor-tests/package.json +++ b/extensions/vscode-custom-editor-tests/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@types/mocha": "^8.2.0", - "@types/node": "14.x", + "@types/node": "16.x", "@types/p-limit": "^2.2.0" }, "contributes": { diff --git a/extensions/vscode-custom-editor-tests/yarn.lock b/extensions/vscode-custom-editor-tests/yarn.lock index 52588b92820..2943060a078 100644 --- a/extensions/vscode-custom-editor-tests/yarn.lock +++ b/extensions/vscode-custom-editor-tests/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/p-limit@^2.2.0": version "2.2.0" diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 3cf93067634..e0a672ca179 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -33,7 +33,7 @@ }, "dependencies": {}, "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "contributes": { "commands": [ diff --git a/extensions/vscode-notebook-tests/yarn.lock b/extensions/vscode-notebook-tests/yarn.lock index 995b2c2f8b3..e724e7fffa3 100644 --- a/extensions/vscode-notebook-tests/yarn.lock +++ b/extensions/vscode-notebook-tests/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index 37fdefbfe2a..da90e7e3809 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -32,7 +32,7 @@ ], "main": "./out/extension", "devDependencies": { - "@types/node": "14.x" + "@types/node": "16.x" }, "capabilities": { "untrustedWorkspaces": { diff --git a/extensions/vscode-test-resolver/src/download.ts b/extensions/vscode-test-resolver/src/download.ts index 805c4948210..fa001b5a178 100644 --- a/extensions/vscode-test-resolver/src/download.ts +++ b/extensions/vscode-test-resolver/src/download.ts @@ -86,7 +86,7 @@ function unzipVSCodeServer(vscodeArchivePath: string, extractDir: string, destDi } else { cp.spawnSync('unzip', [vscodeArchivePath, '-d', `${tempDir}`]); } - fs.renameSync(path.join(tempDir, process.platform === 'win32' ? 'vscode-server-win32-x64' : 'vscode-server-darwin'), extractDir); + fs.renameSync(path.join(tempDir, process.platform === 'win32' ? 'vscode-server-win32-x64' : 'vscode-server-darwin-x64'), extractDir); } else { // tar does not create extractDir by default if (!fs.existsSync(extractDir)) { diff --git a/extensions/vscode-test-resolver/src/util/processes.ts b/extensions/vscode-test-resolver/src/util/processes.ts index 3bc12725522..f16d0678c52 100644 --- a/extensions/vscode-test-resolver/src/util/processes.ts +++ b/extensions/vscode-test-resolver/src/util/processes.ts @@ -16,14 +16,14 @@ export function terminateProcess(p: cp.ChildProcess, extensionPath: string): Ter const options: any = { stdio: ['pipe', 'pipe', 'ignore'] }; - cp.execFileSync('taskkill', ['/T', '/F', '/PID', p.pid.toString()], options); + cp.execFileSync('taskkill', ['/T', '/F', '/PID', p.pid!.toString()], options); } catch (err) { return { success: false, error: err }; } } else if (process.platform === 'darwin' || process.platform === 'linux') { try { const cmd = path.join(extensionPath, 'scripts', 'terminateProcess.sh'); - const result = cp.spawnSync(cmd, [p.pid.toString()]); + const result = cp.spawnSync(cmd, [p.pid!.toString()]); if (result.error) { return { success: false, error: result.error }; } diff --git a/extensions/vscode-test-resolver/yarn.lock b/extensions/vscode-test-resolver/yarn.lock index 995b2c2f8b3..e724e7fffa3 100644 --- a/extensions/vscode-test-resolver/yarn.lock +++ b/extensions/vscode-test-resolver/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 4e779dc4ef4..9ae2538a726 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -24,15 +24,15 @@ fast-plist@0.1.2: resolved "https://registry.yarnpkg.com/fast-plist/-/fast-plist-0.1.2.tgz#a45aff345196006d406ca6cdcd05f69051ef35b8" integrity sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= -typescript@^4.6.1-rc: - version "4.6.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.1-rc.tgz#75baa823a6fca592f358b486acc2039f103ca2af" - integrity sha512-tLPT3GelVfTN9wXPOuPKfY83PkMvgdF3V3gHK/ElNrpQPdLKQ/HMU5cS6+7epYSIF2gne190jzydAW0FLLwU7A== +typescript@4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" + integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== -vscode-grammar-updater@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/vscode-grammar-updater/-/vscode-grammar-updater-1.0.3.tgz#695ccaf0567c6a000005a969cd87ecc3b5c25018" - integrity sha512-V/OnMGyAk7Ldv5NC2p+NovidsAghdfbFFnimEzQ7F/TYIqDLJCVe28RcvaU2gywCSCtxNfS5MYe0egiaRIWNEw== +vscode-grammar-updater@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vscode-grammar-updater/-/vscode-grammar-updater-1.0.4.tgz#f0b8bd106a499a15f3e6b199055908ed8e860984" + integrity sha512-WjmpFo+jlnxOfHNeSrO3nJx8S2u3f926UL0AHJhDMQghCwEfkMvf37aafF83xvtLW2G9ywhifLbq4caxDQm+wQ== dependencies: cson-parser "^1.3.3" fast-plist "0.1.2" diff --git a/package.json b/package.json index 4d678a9b90f..1806f4bb6e4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.65.0", - "distro": "ef03753457af8baec386fedcaf6a6ae105b12ed8", + "version": "1.66.0", + "distro": "6f5536c77b88df5cedc4f1390a1cd1ba3e8f9a83", "author": { "name": "Microsoft Corporation" }, @@ -12,7 +12,7 @@ "test": "echo Please run any of the test scripts from the scripts folder.", "test-browser": "npx playwright install && node test/unit/browser/index.js", "test-browser-no-install": "node test/unit/browser/index.js", - "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --exit", + "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --timeout=5000 --exit", "preinstall": "node build/npm/preinstall.js", "postinstall": "node build/npm/postinstall.js", "compile": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile", @@ -33,7 +33,7 @@ "gulp": "node --max_old_space_size=8192 ./node_modules/gulp/bin/gulp.js", "electron": "node build/lib/electron", "7z": "7z", - "update-grammars": "node build/npm/update-all-grammars.js", + "update-grammars": "node build/npm/update-all-grammars.mjs", "update-localization-extension": "node build/npm/update-localization-extension.js", "smoketest": "node build/lib/preLaunch.js && cd test/smoke && yarn compile && node test/index.js", "smoketest-no-compile": "cd test/smoke && node test/index.js", @@ -43,7 +43,7 @@ "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json", "vscode-dts-compile-check": "tsc -p src/tsconfig.vscode-dts.json && tsc -p src/tsconfig.vscode-proposed-dts.json", "valid-layers-check": "node build/lib/layersChecker.js", - "update-distro": "node build/npm/update-distro.js", + "update-distro": "node build/npm/update-distro.mjs", "web": "echo 'yarn web' is replaced by './scripts/code-server' or './scripts/code-web'", "compile-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-web", "watch-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-web", @@ -61,10 +61,9 @@ "dependencies": { "@microsoft/applicationinsights-web": "^2.6.4", "@parcel/watcher": "2.0.5", - "@vscode/debugprotocol": "1.54.0", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/ripgrep": "^1.14.1", - "@vscode/sqlite3": "4.0.12", + "@vscode/ripgrep": "^1.14.2", + "@vscode/sqlite3": "5.0.7", "@vscode/sudo-prompt": "9.3.1", "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.4.2", @@ -72,11 +71,11 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "jschardet": "3.0.0", - "keytar": "7.6.0", + "keytar": "7.9.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.3", - "native-keymap": "3.2.1", - "native-watchdog": "1.3.0", + "native-keymap": "3.3.0", + "native-watchdog": "1.4.0", "node-pty": "0.11.0-beta11", "spdlog": "^0.13.0", "tas-client-umd": "0.1.4", @@ -85,12 +84,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "6.0.0", - "xterm": "4.18.0-beta.15", - "xterm-addon-search": "0.9.0-beta.10", - "xterm-addon-serialize": "0.7.0-beta.9", + "xterm": "4.19.0-beta.7", + "xterm-addon-search": "0.9.0-beta.11", + "xterm-addon-serialize": "0.7.0-beta.11", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.24", - "xterm-headless": "4.18.0-beta.15", + "xterm-headless": "4.19.0-beta.7", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -109,7 +108,7 @@ "@types/keytar": "^4.4.0", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.0", - "@types/node": "14.x", + "@types/node": "16.x", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^1.0.6", @@ -136,7 +135,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "13.5.2", + "electron": "17.1.2", "eslint": "8.7.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^19.1.0", @@ -200,9 +199,9 @@ "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^1.0.0", - "ts-loader": "^9.2.3", + "ts-loader": "^9.2.7", "tsec": "0.1.4", - "typescript": "^4.6.0-dev.20220209", + "typescript": "^4.7.0-dev.20220316", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/product.json b/product.json index 9d630f23cd7..b01093caf3a 100644 --- a/product.json +++ b/product.json @@ -27,17 +27,11 @@ "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", "urlProtocol": "code-oss", - "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b/out/vs/workbench/contrib/webview/browser/pre/", - "extensionAllowedProposedApi": [ - "ms-vscode.vscode-js-profile-flame", - "ms-vscode.vscode-js-profile-table", - "GitHub.remotehub", - "GitHub.remotehub-insiders" - ], + "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/181b43c0e2949e36ecb623d8cc6de29d4fa2bae8/out/vs/workbench/contrib/webview/browser/pre/", "builtInExtensions": [ { "name": "ms-vscode.references-view", - "version": "0.0.85", + "version": "0.0.86", "repo": "https://github.com/microsoft/vscode-references-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", @@ -52,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.15", + "version": "1.0.16", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -67,7 +61,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.65.0", + "version": "1.66.0", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", @@ -82,7 +76,7 @@ }, { "name": "ms-vscode.vscode-js-profile-table", - "version": "0.0.18", + "version": "1.0.0", "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", "metadata": { "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", diff --git a/remote/.yarnrc b/remote/.yarnrc index 7ee8b67ae99..1df1b5f2441 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,4 +1,4 @@ disturl "http://nodejs.org/dist" -target "14.16.0" +target "16.13.0" runtime "node" build_from_source "true" diff --git a/remote/package.json b/remote/package.json index 47878146a32..946b4e60b9a 100644 --- a/remote/package.json +++ b/remote/package.json @@ -6,7 +6,7 @@ "@microsoft/applicationinsights-web": "^2.6.4", "@parcel/watcher": "2.0.5", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/ripgrep": "^1.14.1", + "@vscode/ripgrep": "^1.14.2", "@vscode/vscode-languagedetection": "1.0.21", "applicationinsights": "1.4.2", "cookie": "^0.4.0", @@ -14,9 +14,9 @@ "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "jschardet": "3.0.0", - "keytar": "7.6.0", + "keytar": "7.9.0", "minimist": "^1.2.5", - "native-watchdog": "1.3.0", + "native-watchdog": "1.4.0", "node-pty": "0.11.0-beta11", "spdlog": "^0.13.0", "tas-client-umd": "0.1.4", @@ -24,12 +24,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "6.0.0", - "xterm": "4.18.0-beta.15", - "xterm-addon-search": "0.9.0-beta.10", - "xterm-addon-serialize": "0.7.0-beta.9", + "xterm": "4.19.0-beta.7", + "xterm-addon-search": "0.9.0-beta.11", + "xterm-addon-serialize": "0.7.0-beta.11", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.24", - "xterm-headless": "4.18.0-beta.15", + "xterm-headless": "4.19.0-beta.7", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index c5d937caac7..a2ed27f14d0 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -10,8 +10,8 @@ "tas-client-umd": "0.1.4", "vscode-oniguruma": "1.6.1", "vscode-textmate": "6.0.0", - "xterm": "4.18.0-beta.15", - "xterm-addon-search": "0.9.0-beta.10", + "xterm": "4.19.0-beta.7", + "xterm-addon-search": "0.9.0-beta.11", "xterm-addon-unicode11": "0.4.0-beta.3", "xterm-addon-webgl": "0.12.0-beta.24" } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 32714eaae4a..29e3b9d4393 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -113,10 +113,10 @@ vscode-textmate@6.0.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-6.0.0.tgz#a3777197235036814ac9a92451492f2748589210" integrity sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ== -xterm-addon-search@0.9.0-beta.10: - version "0.9.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.10.tgz#80677a5e105d4410feae92b90fcdd5b538067070" - integrity sha512-fxKwbsu+ZNgZ689sAX1PHhWAW+8/abAGD8B7SMWwelKhJmbRybHoaLAYCeUrZJlJHljwjgW3Ptk7OpONNydh1A== +xterm-addon-search@0.9.0-beta.11: + version "0.9.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.11.tgz#e6af723903e470564682eb80e5f0ca7e5d291c60" + integrity sha512-sUaOrgqFJvN7oI20ruKVOEqN5t4UK9q/pAR7FSjH5vVl6h0Hhtndh9bhrgoKCSe+Do1YfZCcpWx0Sbs9Y+2Ptg== xterm-addon-unicode11@0.4.0-beta.3: version "0.4.0-beta.3" @@ -128,7 +128,7 @@ xterm-addon-webgl@0.12.0-beta.24: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.24.tgz#5c17256933991856554c95c9bd1eaab42e9727a0" integrity sha512-+wZxKReEOlfN9JRHyikoffA6Do61/THR7QY35ajkQo0lLutKr6hTd/TLTuZh0PhFVelgTgudpXqlP++Lc0WFIA== -xterm@4.18.0-beta.15: - version "4.18.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0-beta.15.tgz#99d40cfbd2e7f3343b2a125fd7d4b3bb864ca2f5" - integrity sha512-e3JkreKDjXNZcpQsHybaroTGXTtq7Lu1Bx+wuviBBllhz9CxI+uHzwMNHPgdFaZ+zwJq85hyeVHn354ooJ8nzA== +xterm@4.19.0-beta.7: + version "4.19.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.7.tgz#25165366e005876d1e11418989b88687530ad902" + integrity sha512-BusEhdm+7Dwhtilk67mEISfYzrwJYXLgN+N+jbwVPZqDR9/CwPoG/cq6InibLAvciK1JCBwzSB32XeHFtZskWA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 7fa9e60d8f0..813e67bf6cc 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -101,12 +101,12 @@ resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/ripgrep@^1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.1.tgz#66b104a6c7283d17672eb01c02b1c1294d4bb2ae" - integrity sha512-f2N/iPZhxP9bho7iK0DibJDprU+spE8hTIvQg1fi8v82oWIWU9IB4a92444GyxSaFgb+hWpQe46QkFDh5W1VpQ== +"@vscode/ripgrep@^1.14.2": + version "1.14.2" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34" + integrity sha512-KDaehS8Jfdg1dqStaIPDKYh66jzKd5jy5aYEPzIv0JYFLADPsCSQPBUdsJVXnr0t72OlDcj96W05xt/rSnNFFQ== dependencies: - https-proxy-agent "^4.0.0" + https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" "@vscode/vscode-languagedetection@1.0.21": @@ -126,11 +126,6 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@5: - version "5.1.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" - integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== - agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" @@ -290,12 +285,12 @@ debug@^4.3.1: dependencies: ms "2.1.2" -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: - mimic-response "^2.0.0" + mimic-response "^3.1.0" deep-extend@^0.6.0: version "0.6.0" @@ -307,10 +302,10 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== diagnostic-channel-publishers@^0.3.3: version "0.3.5" @@ -431,14 +426,6 @@ https-proxy-agent@^2.2.3: agent-base "^4.3.0" debug "^3.1.0" -https-proxy-agent@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" - integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== - dependencies: - agent-base "5" - debug "4" - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -489,18 +476,25 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== -keytar@7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.6.0.tgz#498e796443cb543d31722099443f29d7b5c44100" - integrity sha512-H3cvrTzWb11+iv0NOAnoNAPgEapVZnYLVHZQyxmh7jdmVfR/c0jNNFEZ6AI38W/4DeTGTaY66ZX4Z1SbfKPvCQ== +keytar@7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== dependencies: - node-addon-api "^3.0.0" - prebuild-install "^6.0.0" + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" @@ -544,28 +538,33 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== -native-watchdog@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.3.0.tgz#88cee94c9dc766b85c8506eda14c8bd8c9618e27" - integrity sha512-WOjGRNGkYZ5MXsntcvCYrKtSYMaewlbCFplbcUVo9bE80LPVt8TAVFHYWB8+a6fWCGYheq21+Wtt6CJrUaCJhw== +native-watchdog@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.4.0.tgz#547a1f9f88754c38089c622d405ed1e324c7a545" + integrity sha512-4FynAeGtTpoQ2+5AxVJXGEGsOzPsNYDh8Xmawjgs7YWJe+bbbgt7CYlA/Qx6X+kwtN5Ey1aNSm9MqZa0iNKkGw== -node-abi@^2.21.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== +node-abi@^3.3.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32" + integrity sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw== dependencies: - semver "^5.4.1" - -node-addon-api@^3.0.0, node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + semver "^7.3.5" node-addon-api@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== +node-addon-api@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + node-gyp-build@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" @@ -610,22 +609,22 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -prebuild-install@^6.0.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== +prebuild-install@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" + integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg== dependencies: - detect-libc "^1.0.3" + 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 "^2.21.0" + node-abi "^3.3.0" npmlog "^4.0.1" pump "^3.0.0" rc "^1.2.7" - simple-get "^3.0.3" + simple-get "^4.0.0" tar-fs "^2.0.0" tunnel-agent "^0.6.0" @@ -699,6 +698,13 @@ semver@^5.4.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -719,12 +725,12 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: - decompress-response "^4.2.0" + decompress-response "^6.0.0" once "^1.3.1" simple-concat "^1.0.0" @@ -908,15 +914,15 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-search@0.9.0-beta.10: - version "0.9.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.10.tgz#80677a5e105d4410feae92b90fcdd5b538067070" - integrity sha512-fxKwbsu+ZNgZ689sAX1PHhWAW+8/abAGD8B7SMWwelKhJmbRybHoaLAYCeUrZJlJHljwjgW3Ptk7OpONNydh1A== +xterm-addon-search@0.9.0-beta.11: + version "0.9.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.11.tgz#e6af723903e470564682eb80e5f0ca7e5d291c60" + integrity sha512-sUaOrgqFJvN7oI20ruKVOEqN5t4UK9q/pAR7FSjH5vVl6h0Hhtndh9bhrgoKCSe+Do1YfZCcpWx0Sbs9Y+2Ptg== -xterm-addon-serialize@0.7.0-beta.9: - version "0.7.0-beta.9" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.9.tgz#122dcc1b764fad1a9970079c690e0810c7936a2d" - integrity sha512-a9DIC624nmJKcY8e8ykzib9Lefli/ea/87JFtUBDZPe5w12NM0XlQgDkcyCS1MhcTQKA1RYoEp9003QDkmIybQ== +xterm-addon-serialize@0.7.0-beta.11: + version "0.7.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.11.tgz#d9d6a862b3d70fc6160e39bc235a2a9ef45e20d6" + integrity sha512-okirLeH8VpOxlnZkER2lN0emaTgWe/DHVGpY9ByfLIoYw506Ci2LRcOjyYwJBRTJcfLp3TGnwQSYfooCZHDndw== xterm-addon-unicode11@0.4.0-beta.3: version "0.4.0-beta.3" @@ -928,15 +934,20 @@ xterm-addon-webgl@0.12.0-beta.24: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.24.tgz#5c17256933991856554c95c9bd1eaab42e9727a0" integrity sha512-+wZxKReEOlfN9JRHyikoffA6Do61/THR7QY35ajkQo0lLutKr6hTd/TLTuZh0PhFVelgTgudpXqlP++Lc0WFIA== -xterm-headless@4.18.0-beta.15: - version "4.18.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.18.0-beta.15.tgz#f710959af0aea37f8395a8011e431f2341405473" - integrity sha512-uIrWvVSdZzDneDPD9/u4mZ+OvnPDpsI/bvUzQ923/9GEyLO2UlJGipJUBhMB5W+xhox5PZJIAJJxCHnXo8Vx2Q== +xterm-headless@4.19.0-beta.7: + version "4.19.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.7.tgz#82de331ec183bfe8758250617b1dff700dc9380a" + integrity sha512-wLzw3Kro1UYXLd4ytk7mISrj7IytEAVCHEuk+Cdckknh+HiX1zFU351uOOh+IqpElIbibgor8kqB5ZDuptfaYw== -xterm@4.18.0-beta.15: - version "4.18.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0-beta.15.tgz#99d40cfbd2e7f3343b2a125fd7d4b3bb864ca2f5" - integrity sha512-e3JkreKDjXNZcpQsHybaroTGXTtq7Lu1Bx+wuviBBllhz9CxI+uHzwMNHPgdFaZ+zwJq85hyeVHn354ooJ8nzA== +xterm@4.19.0-beta.7: + version "4.19.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.7.tgz#25165366e005876d1e11418989b88687530ad902" + integrity sha512-BusEhdm+7Dwhtilk67mEISfYzrwJYXLgN+N+jbwVPZqDR9/CwPoG/cq6InibLAvciK1JCBwzSB32XeHFtZskWA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yauzl@^2.9.2: version "2.10.0" diff --git a/resources/linux/rpm/dependencies.json b/resources/linux/rpm/dependencies.json deleted file mode 100644 index 8a055565b64..00000000000 --- a/resources/linux/rpm/dependencies.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "x86_64": [ - "libpthread.so.0()(64bit)", - "libpthread.so.0(GLIBC_2.2.5)(64bit)", - "libpthread.so.0(GLIBC_2.3.2)(64bit)", - "libpthread.so.0(GLIBC_2.3.3)(64bit)", - "libgtk-3.so.0()(64bit)", - "libgdk-x11-2.0.so.0()(64bit)", - "libatk-1.0.so.0()(64bit)", - "libgio-2.0.so.0()(64bit)", - "libpangocairo-1.0.so.0()(64bit)", - "libgdk_pixbuf-2.0.so.0()(64bit)", - "libcairo.so.2()(64bit)", - "libpango-1.0.so.0()(64bit)", - "libfreetype.so.6()(64bit)", - "libfontconfig.so.1()(64bit)", - "libgobject-2.0.so.0()(64bit)", - "libdbus-1.so.3()(64bit)", - "libXi.so.6()(64bit)", - "libXcursor.so.1()(64bit)", - "libXdamage.so.1()(64bit)", - "libXrandr.so.2()(64bit)", - "libXcomposite.so.1()(64bit)", - "libXext.so.6()(64bit)", - "libXfixes.so.3()(64bit)", - "libXrender.so.1()(64bit)", - "libX11.so.6()(64bit)", - "libXss.so.1()(64bit)", - "libXtst.so.6()(64bit)", - "libgmodule-2.0.so.0()(64bit)", - "librt.so.1()(64bit)", - "libglib-2.0.so.0()(64bit)", - "libnss3.so()(64bit)", - "libnssutil3.so()(64bit)", - "libsmime3.so()(64bit)", - "libnspr4.so()(64bit)", - "libasound.so.2()(64bit)", - "libcups.so.2()(64bit)", - "libdl.so.2()(64bit)", - "libexpat.so.1()(64bit)", - "libstdc++.so.6()(64bit)", - "libstdc++.so.6(GLIBCXX_3.4)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.10)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.11)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.14)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.15)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.9)(64bit)", - "libm.so.6()(64bit)", - "libm.so.6(GLIBC_2.2.5)(64bit)", - "libgcc_s.so.1()(64bit)", - "libgcc_s.so.1(GCC_3.0)(64bit)", - "libgcc_s.so.1(GCC_4.0.0)(64bit)", - "libc.so.6()(64bit)", - "libc.so.6(GLIBC_2.11)(64bit)", - "libc.so.6(GLIBC_2.2.5)(64bit)", - "libc.so.6(GLIBC_2.3)(64bit)", - "libc.so.6(GLIBC_2.3.2)(64bit)", - "libc.so.6(GLIBC_2.3.4)(64bit)", - "libc.so.6(GLIBC_2.4)(64bit)", - "libc.so.6(GLIBC_2.6)(64bit)", - "libc.so.6(GLIBC_2.7)(64bit)", - "libc.so.6(GLIBC_2.9)(64bit)", - "libxcb.so.1()(64bit)", - "libxkbfile.so.1()(64bit)", - "libsecret-1.so.0()(64bit)", - "libgbm.so.1()(64bit)" - ], - "aarch64": [ - "libpthread.so.0()(64bit)", - "libpthread.so.0(GLIBC_2.17)(64bit)", - "libgtk-3.so.0()(64bit)", - "libgdk-x11-2.0.so.0()(64bit)", - "libatk-1.0.so.0()(64bit)", - "libgio-2.0.so.0()(64bit)", - "libpangocairo-1.0.so.0()(64bit)", - "libgdk_pixbuf-2.0.so.0()(64bit)", - "libcairo.so.2()(64bit)", - "libpango-1.0.so.0()(64bit)", - "libfreetype.so.6()(64bit)", - "libfontconfig.so.1()(64bit)", - "libgobject-2.0.so.0()(64bit)", - "libdbus-1.so.3()(64bit)", - "libXi.so.6()(64bit)", - "libXcursor.so.1()(64bit)", - "libXdamage.so.1()(64bit)", - "libXrandr.so.2()(64bit)", - "libXcomposite.so.1()(64bit)", - "libXext.so.6()(64bit)", - "libXfixes.so.3()(64bit)", - "libXrender.so.1()(64bit)", - "libX11.so.6()(64bit)", - "libXss.so.1()(64bit)", - "libXtst.so.6()(64bit)", - "libgmodule-2.0.so.0()(64bit)", - "librt.so.1()(64bit)", - "libglib-2.0.so.0()(64bit)", - "libnss3.so()(64bit)", - "libnssutil3.so()(64bit)", - "libsmime3.so()(64bit)", - "libnspr4.so()(64bit)", - "libasound.so.2()(64bit)", - "libcups.so.2()(64bit)", - "libdl.so.2()(64bit)", - "libexpat.so.1()(64bit)", - "libstdc++.so.6()(64bit)", - "libstdc++.so.6(GLIBCXX_3.4)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.10)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.11)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.14)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.15)(64bit)", - "libstdc++.so.6(GLIBCXX_3.4.9)(64bit)", - "libm.so.6()(64bit)", - "libm.so.6(GLIBC_2.17)(64bit)", - "libgcc_s.so.1()(64bit)", - "libgcc_s.so.1(GCC_3.0)(64bit)", - "libgcc_s.so.1(GCC_4.0.0)(64bit)", - "libc.so.6()(64bit)", - "libc.so.6(GLIBC_2.17)(64bit)", - "libxcb.so.1()(64bit)", - "libxkbfile.so.1()(64bit)", - "libsecret-1.so.0()(64bit)" - ], - "armv7hl": [ - "libpthread.so.0()", - "libpthread.so.0(GLIBC_2.4)", - "libpthread.so.0(GLIBC_2.11)", - "libpthread.so.0(GLIBC_2.12)", - "libgtk-3.so.0()", - "libgdk-x11-2.0.so.0()", - "libatk-1.0.so.0()", - "libgio-2.0.so.0()", - "libpangocairo-1.0.so.0()", - "libgdk_pixbuf-2.0.so.0()", - "libcairo.so.2()", - "libpango-1.0.so.0()", - "libfreetype.so.6()", - "libfontconfig.so.1()", - "libgobject-2.0.so.0()", - "libdbus-1.so.3()", - "libXi.so.6()", - "libXcursor.so.1()", - "libXdamage.so.1()", - "libXrandr.so.2()", - "libXcomposite.so.1()", - "libXext.so.6()", - "libXfixes.so.3()", - "libXrender.so.1()", - "libX11.so.6()", - "libXss.so.1()", - "libXtst.so.6()", - "libgmodule-2.0.so.0()", - "librt.so.1()", - "libglib-2.0.so.0()", - "libnss3.so()", - "libnssutil3.so()", - "libsmime3.so()", - "libnspr4.so()", - "libasound.so.2()", - "libcups.so.2()", - "libdl.so.2()", - "libexpat.so.1()", - "libstdc++.so.6()", - "libstdc++.so.6(GLIBCXX_3.4)", - "libstdc++.so.6(GLIBCXX_3.4.10)", - "libstdc++.so.6(GLIBCXX_3.4.11)", - "libstdc++.so.6(GLIBCXX_3.4.14)", - "libstdc++.so.6(GLIBCXX_3.4.15)", - "libstdc++.so.6(GLIBCXX_3.4.9)", - "libm.so.6()", - "libm.so.6(GLIBC_2.4)", - "libm.so.6(GLIBC_2.15)", - "libgcc_s.so.1()", - "libgcc_s.so.1(GCC_3.0)", - "libgcc_s.so.1(GCC_4.0.0)", - "libc.so.6()", - "libc.so.6(GLIBC_2.11)", - "libc.so.6(GLIBC_2.4)", - "libc.so.6(GLIBC_2.6)", - "libc.so.6(GLIBC_2.7)", - "libc.so.6(GLIBC_2.9)", - "libxcb.so.1()", - "libxkbfile.so.1()", - "libsecret-1.so.0()" - ] -} diff --git a/resources/server/bin/code-server-darwin.sh b/resources/server/bin/code-server-darwin.sh index 6f109c38432..04c5d7475da 100644 --- a/resources/server/bin/code-server-darwin.sh +++ b/resources/server/bin/code-server-darwin.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash # # Copyright (c) Microsoft Corporation. All rights reserved. # @@ -7,7 +7,17 @@ case "$1" in --inspect*) INSPECT="$1"; shift;; esac -function realpath() { python -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$0"; } -ROOT=$(dirname $(dirname $(realpath "$0"))) +realdir() { + SOURCE=$1 + while [ -h "$SOURCE" ]; do + DIR=$(dirname "$SOURCE") + SOURCE=$(readlink "$SOURCE") + [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE + done + echo "$( cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd )" +} + +ROOT=$(dirname "$(realdir "$0")") "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" + diff --git a/resources/server/bin/code-server-linux.sh b/resources/server/bin/code-server-linux.sh index b162cc656b8..3df32dfd43c 100644 --- a/resources/server/bin/code-server-linux.sh +++ b/resources/server/bin/code-server-linux.sh @@ -7,6 +7,6 @@ case "$1" in --inspect*) INSPECT="$1"; shift;; esac -ROOT=$(dirname $(dirname $(readlink -f $0))) +ROOT="$(dirname "$(dirname "$(readlink -f "$0")")")" "$ROOT/node" ${INSPECT:-} "$ROOT/out/server-main.js" "$@" diff --git a/scripts/code-server.bat b/scripts/code-server.bat index bba4c58d3c1..23006f829ee 100644 --- a/scripts/code-server.bat +++ b/scripts/code-server.bat @@ -3,7 +3,9 @@ setlocal title VSCode Server -pushd %~dp0\.. +set ROOT_DIR=%~dp0.. + +pushd %ROOT_DIR% :: Configuration set NODE_ENV=development @@ -20,9 +22,10 @@ if not exist "%NODE%" ( call yarn gulp node ) -:: Launch Server -call "%NODE%" scripts\code-server.js %* - popd +:: Launch Server +call "%NODE%" %ROOT_DIR%\scripts\code-server.js %* + + endlocal diff --git a/scripts/code-server.js b/scripts/code-server.js index f4d34131cb3..8f2c630fc1f 100644 --- a/scripts/code-server.js +++ b/scripts/code-server.js @@ -8,23 +8,15 @@ const cp = require('child_process'); const path = require('path'); const opn = require('opn'); -const crypto = require('crypto'); const minimist = require('minimist'); -function main() { +async function main() { const args = minimist(process.argv.slice(2), { boolean: [ 'help', 'launch' - ], - string: [ - 'host', - 'port', - 'driver', - 'connection-token', - 'server-data-dir' - ], + ] }); if (args.help) { @@ -36,46 +28,42 @@ function main() { return; } + process.env['VSCODE_SERVER_PORT'] = '9888'; + const serverArgs = process.argv.slice(2).filter(v => v !== '--launch'); - - const HOST = args['host'] ?? 'localhost'; - const PORT = args['port'] ?? '9888'; - const TOKEN = args['connection-token'] ?? String(crypto.randomInt(0xffffffff)); - - if (args['connection-token'] === undefined && args['connection-token-file'] === undefined && !args['without-connection-token']) { - serverArgs.push('--connection-token', TOKEN); - } - if (args['host'] === undefined) { - serverArgs.push('--host', HOST); - } - if (args['port'] === undefined) { - serverArgs.push('--port', PORT); - } - - startServer(serverArgs); + const addr = await startServer(serverArgs); if (args['launch']) { - opn(`http://${HOST}:${PORT}/?tkn=${TOKEN}`); + opn(addr); } } function startServer(programArgs) { - const env = { ...process.env }; + return new Promise((s, e) => { + const env = { ...process.env }; + const entryPoint = path.join(__dirname, '..', 'out', 'server-main.js'); - const entryPoint = path.join(__dirname, '..', 'out', 'server-main.js'); + console.log(`Starting server: ${entryPoint} ${programArgs.join(' ')}`); + const proc = cp.spawn(process.execPath, [entryPoint, ...programArgs], { env, stdio: [process.stdin, null, process.stderr] }); + proc.stdout.on('data', e => { + const data = e.toString(); + console.log(data); + const m = data.match(/Web UI available at (.*)/); + if (m) { + s(m[1]); + } + }); - console.log(`Starting server: ${entryPoint} ${programArgs.join(' ')}`); - const proc = cp.spawn(process.execPath, [entryPoint, ...programArgs], { env, stdio: 'inherit' }); + proc.on('exit', (code) => process.exit(code)); - proc.on('exit', (code) => process.exit(code)); - - process.on('exit', () => proc.kill()); - process.on('SIGINT', () => { - proc.kill(); - process.exit(128 + 2); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events - }); - process.on('SIGTERM', () => { - proc.kill(); - process.exit(128 + 15); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events + process.on('exit', () => proc.kill()); + process.on('SIGINT', () => { + proc.kill(); + process.exit(128 + 2); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events + }); + process.on('SIGTERM', () => { + proc.kill(); + process.exit(128 + 15); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events + }); }); } diff --git a/scripts/code-server.sh b/scripts/code-server.sh index 298157e4ee7..bc910a9bb98 100755 --- a/scripts/code-server.sh +++ b/scripts/code-server.sh @@ -8,7 +8,7 @@ else fi function code() { - cd $ROOT + pushd $ROOT # Get electron, compile, built-in extensions if [[ -z "${VSCODE_SKIP_PRELAUNCH}" ]]; then @@ -21,9 +21,11 @@ function code() { yarn gulp node fi + popd + NODE_ENV=development \ VSCODE_DEV=1 \ - $NODE ./scripts/code-server.js "$@" + $NODE $ROOT/scripts/code-server.js "$@" } code "$@" diff --git a/src/buildfile.js b/src/buildfile.js index f5df248ba21..4a174a51436 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -3,7 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const { createModuleDescription, createEditorWorkerModuleDescription } = require('./vs/base/buildfile'); +/** + * @param {string} name + * @param {string[]} exclude + */ +function createModuleDescription(name, exclude) { + + let excludes = ['vs/css', 'vs/nls']; + if (Array.isArray(exclude) && exclude.length > 0) { + excludes = excludes.concat(exclude); + } + + return { + name: name, + include: [], + exclude: excludes + }; +} + +/** + * @param {string} name + */ +function createEditorWorkerModuleDescription(name) { + return createModuleDescription(name, ['vs/base/common/worker/simpleWorker', 'vs/editor/common/services/editorSimpleWorker']); +} exports.base = [ { @@ -28,8 +51,18 @@ exports.workerSharedProcess = [createEditorWorkerModuleDescription('vs/platform/ exports.workerLanguageDetection = [createEditorWorkerModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker')]; exports.workerLocalFileSearch = [createEditorWorkerModuleDescription('vs/workbench/services/search/worker/localFileSearch')]; -exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules(); -exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules(); +exports.workbenchDesktop = [ + createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), + createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), + createModuleDescription('vs/platform/files/node/watcher/watcherMain'), + createModuleDescription('vs/platform/terminal/node/ptyHostMain'), + createModuleDescription('vs/workbench/api/node/extensionHostProcess') +]; + +exports.workbenchWeb = [ + createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), + createModuleDescription('vs/code/browser/workbench/workbench', ['vs/workbench/workbench.web.main']) +]; exports.keyboardMaps = [ createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'), @@ -37,6 +70,14 @@ exports.keyboardMaps = [ createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win') ]; -exports.code = require('./vs/code/buildfile').collectModules(); +exports.code = [ + createModuleDescription('vs/code/electron-main/main'), + createModuleDescription('vs/code/node/cli'), + createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), + createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain'), + createModuleDescription('vs/code/electron-browser/sharedProcess/sharedProcessMain'), + createModuleDescription('vs/platform/driver/node/driver'), + createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain') +]; exports.entrypoint = createModuleDescription; diff --git a/src/main.js b/src/main.js index 9419e5ae33f..c9347d4516b 100644 --- a/src/main.js +++ b/src/main.js @@ -26,8 +26,6 @@ const { stripComments } = require('./vs/base/common/stripComments'); const product = require('../product.json'); const { app, protocol, crashReporter } = require('electron'); -app.allowRendererProcessReuse = false; - // Enable portable support const portable = bootstrapNode.configurePortable(product); diff --git a/src/server-main.js b/src/server-main.js index 9875e6bf193..d8e5fdd45dd 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -24,6 +24,14 @@ async function start() { string: ['install-extension', 'install-builtin-extension', 'uninstall-extension', 'locate-extension', 'socket-path', 'host', 'port', 'pick-port', 'compatibility'], alias: { help: 'h', version: 'v' } }); + ['host', 'port', 'accept-server-license-terms'].forEach(e => { + if (!parsedArgs[e]) { + const envValue = process.env[`VSCODE_SERVER_${e.toUpperCase().replace('-', '_')}`]; + if (envValue) { + parsedArgs[e] = envValue; + } + } + }); const extensionLookupArgs = ['list-extensions', 'locate-extension']; const extensionInstallArgs = ['install-extension', 'install-builtin-extension', 'uninstall-extension']; @@ -66,16 +74,16 @@ async function start() { if (product.serverLicensePrompt && parsedArgs['accept-server-license-terms'] !== true) { if (hasStdinWithoutTty()) { console.log('To accept the license terms, start the server with --accept-server-license-terms'); - process.exit(); + process.exit(1); } try { const accept = await prompt(product.serverLicensePrompt); if (!accept) { - process.exit(); + process.exit(1); } } catch (e) { console.log(e); - process.exit(); + process.exit(1); } } } diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json index 02f70161510..d5ce3f010a9 100644 --- a/src/tsec.exemptions.json +++ b/src/tsec.exemptions.json @@ -22,7 +22,7 @@ "vs/editor/browser/widget/diffReview.ts", "vs/editor/standalone/browser/colorizer.ts", "vs/workbench/api/worker/extHostExtensionService.ts", - "vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts", + "vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts", "vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts", "vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts" ], diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 1c60612b0e2..d41cafcdb77 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, markAsSingleton } from 'vs/base/common/lifecycle'; class WindowManager { @@ -124,7 +124,7 @@ class PixelRatioFacade { private _pixelRatioMonitor: PixelRatioImpl | null = null; private _getOrCreatePixelRatioMonitor(): PixelRatioImpl { if (!this._pixelRatioMonitor) { - this._pixelRatioMonitor = new PixelRatioImpl(); + this._pixelRatioMonitor = markAsSingleton(new PixelRatioImpl()); } return this._pixelRatioMonitor; } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 4285492c9b3..f465dc9d495 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -73,6 +73,9 @@ export interface IAddStandardDisposableListenerSignature { (node: HTMLElement, type: 'keydown', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; (node: HTMLElement, type: 'keypress', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; (node: HTMLElement, type: 'keyup', handler: (event: IKeyboardEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: 'pointerdown', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: 'pointermove', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; + (node: HTMLElement, type: 'pointerup', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable; (node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable; } function _wrapAsStandardMouseEvent(handler: (e: IMouseEvent) => void): (e: MouseEvent) => void { @@ -1414,6 +1417,49 @@ export function detectFullscreen(): IDetectedFullscreen | null { // -- sanitize and trusted html +/** + * Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src` + * attributes are valid. + */ +export function hookDomPurifyHrefAndSrcSanitizer(allowedProtocols: readonly string[], allowDataImages = false): IDisposable { + // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html + + // build an anchor to map URLs to + const anchor = document.createElement('a'); + + dompurify.addHook('afterSanitizeAttributes', (node) => { + // check all href/src attributes for validity + for (const attr of ['href', 'src']) { + if (node.hasAttribute(attr)) { + const attrValue = node.getAttribute(attr) as string; + if (attr === 'href' && attrValue.startsWith('#')) { + // Allow fragment links + continue; + } + + anchor.href = attrValue; + if (!allowedProtocols.includes(anchor.protocol.replace(/:$/, ''))) { + if (allowDataImages && attr === 'src' && anchor.href.startsWith('data:')) { + continue; + } + + node.removeAttribute(attr); + } + } + } + }); + + return toDisposable(() => { + dompurify.removeHook('afterSanitizeAttributes'); + }); +} + +const defaultSafeProtocols = [ + Schemas.http, + Schemas.https, + Schemas.command, +]; + /** * Sanitizes the given `value` and reset the given `node` with it. */ @@ -1425,29 +1471,12 @@ export function safeInnerHtml(node: HTMLElement, value: string): void { RETURN_DOM_FRAGMENT: false, }; - const allowedProtocols = [Schemas.http, Schemas.https, Schemas.command]; - - // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html - dompurify.addHook('afterSanitizeAttributes', (node) => { - // build an anchor to map URLs to - const anchor = document.createElement('a'); - - // check all href/src attributes for validity - for (const attr in ['href', 'src']) { - if (node.hasAttribute(attr)) { - anchor.href = node.getAttribute(attr) as string; - if (!allowedProtocols.includes(anchor.protocol)) { - node.removeAttribute(attr); - } - } - } - }); - + const hook = hookDomPurifyHrefAndSrcSanitizer(defaultSafeProtocols); try { const html = dompurify.sanitize(value, { ...options, RETURN_TRUSTED_TYPE: true }); node.innerHTML = html as unknown as string; } finally { - dompurify.removeHook('afterSanitizeAttributes'); + hook.dispose(); } } @@ -1648,3 +1677,62 @@ export const enum ZIndex { ModalDialog = 2600, PaneDropOverlay = 10000 } + + +export interface IDragAndDropObserverCallbacks { + readonly onDragEnter: (e: DragEvent) => void; + readonly onDragLeave: (e: DragEvent) => void; + readonly onDrop: (e: DragEvent) => void; + readonly onDragEnd: (e: DragEvent) => void; + + readonly onDragOver?: (e: DragEvent) => void; +} + +export class DragAndDropObserver extends Disposable { + + // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE + // calls see https://github.com/microsoft/vscode/issues/14470 + // when the element has child elements where the events are fired + // repeadedly. + private counter: number = 0; + + constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => { + this.counter++; + + this.callbacks.onDragEnter(e); + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => { + e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) + + if (this.callbacks.onDragOver) { + this.callbacks.onDragOver(e); + } + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => { + this.counter--; + + if (this.counter === 0) { + this.callbacks.onDragLeave(e); + } + })); + + this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => { + this.counter = 0; + this.callbacks.onDragEnd(e); + })); + + this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => { + this.counter = 0; + this.callbacks.onDrop(e); + })); + } +} diff --git a/src/vs/base/browser/fastDomNode.ts b/src/vs/base/browser/fastDomNode.ts index cbc959e308d..30122d0c4ad 100644 --- a/src/vs/base/browser/fastDomNode.ts +++ b/src/vs/base/browser/fastDomNode.ts @@ -5,21 +5,21 @@ export class FastDomNode { - private _maxWidth: number = -1; - private _width: number = -1; - private _height: number = -1; - private _top: number = -1; - private _left: number = -1; - private _bottom: number = -1; - private _right: number = -1; + private _maxWidth: string = ''; + private _width: string = ''; + private _height: string = ''; + private _top: string = ''; + private _left: string = ''; + private _bottom: string = ''; + private _right: string = ''; private _fontFamily: string = ''; private _fontWeight: string = ''; - private _fontSize: number = -1; + private _fontSize: string = ''; private _fontStyle: string = ''; private _fontFeatureSettings: string = ''; private _textDecoration: string = ''; - private _lineHeight: number = -1; - private _letterSpacing: number = -100; + private _lineHeight: string = ''; + private _letterSpacing: string = ''; private _className: string = ''; private _display: string = ''; private _position: string = ''; @@ -34,68 +34,67 @@ export class FastDomNode { public readonly domNode: T ) { } - public setMaxWidth(maxWidth: number): void { + public setMaxWidth(_maxWidth: number | string): void { + const maxWidth = numberAsPixels(_maxWidth); if (this._maxWidth === maxWidth) { return; } this._maxWidth = maxWidth; - this.domNode.style.maxWidth = this._maxWidth + 'px'; + this.domNode.style.maxWidth = this._maxWidth; } - public setWidth(width: number): void { + public setWidth(_width: number | string): void { + const width = numberAsPixels(_width); if (this._width === width) { return; } this._width = width; - this.domNode.style.width = this._width + 'px'; + this.domNode.style.width = this._width; } - public setHeight(height: number): void { + public setHeight(_height: number | string): void { + const height = numberAsPixels(_height); if (this._height === height) { return; } this._height = height; - this.domNode.style.height = this._height + 'px'; + this.domNode.style.height = this._height; } - public setTop(top: number): void { + public setTop(_top: number | string): void { + const top = numberAsPixels(_top); if (this._top === top) { return; } this._top = top; - this.domNode.style.top = this._top + 'px'; + this.domNode.style.top = this._top; } - public unsetTop(): void { - if (this._top === -1) { - return; - } - this._top = -1; - this.domNode.style.top = ''; - } - - public setLeft(left: number): void { + public setLeft(_left: number | string): void { + const left = numberAsPixels(_left); if (this._left === left) { return; } this._left = left; - this.domNode.style.left = this._left + 'px'; + this.domNode.style.left = this._left; } - public setBottom(bottom: number): void { + public setBottom(_bottom: number | string): void { + const bottom = numberAsPixels(_bottom); if (this._bottom === bottom) { return; } this._bottom = bottom; - this.domNode.style.bottom = this._bottom + 'px'; + this.domNode.style.bottom = this._bottom; } - public setRight(right: number): void { + public setRight(_right: number | string): void { + const right = numberAsPixels(_right); if (this._right === right) { return; } this._right = right; - this.domNode.style.right = this._right + 'px'; + this.domNode.style.right = this._right; } public setFontFamily(fontFamily: string): void { @@ -114,12 +113,13 @@ export class FastDomNode { this.domNode.style.fontWeight = this._fontWeight; } - public setFontSize(fontSize: number): void { + public setFontSize(_fontSize: number | string): void { + const fontSize = numberAsPixels(_fontSize); if (this._fontSize === fontSize) { return; } this._fontSize = fontSize; - this.domNode.style.fontSize = this._fontSize + 'px'; + this.domNode.style.fontSize = this._fontSize; } public setFontStyle(fontStyle: string): void { @@ -146,20 +146,22 @@ export class FastDomNode { this.domNode.style.textDecoration = this._textDecoration; } - public setLineHeight(lineHeight: number): void { + public setLineHeight(_lineHeight: number | string): void { + const lineHeight = numberAsPixels(_lineHeight); if (this._lineHeight === lineHeight) { return; } this._lineHeight = lineHeight; - this.domNode.style.lineHeight = this._lineHeight + 'px'; + this.domNode.style.lineHeight = this._lineHeight; } - public setLetterSpacing(letterSpacing: number): void { + public setLetterSpacing(_letterSpacing: number | string): void { + const letterSpacing = numberAsPixels(_letterSpacing); if (this._letterSpacing === letterSpacing) { return; } this._letterSpacing = letterSpacing; - this.domNode.style.letterSpacing = this._letterSpacing + 'px'; + this.domNode.style.letterSpacing = this._letterSpacing; } public setClassName(className: string): void { @@ -256,6 +258,10 @@ export class FastDomNode { } } +function numberAsPixels(value: number | string): string { + return (typeof value === 'number' ? `${value}px` : value); +} + export function createFastDomNode(domNode: T): FastDomNode { return new FastDomNode(domNode); } diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts deleted file mode 100644 index 46298cd6d51..00000000000 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ /dev/null @@ -1,139 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from 'vs/base/browser/dom'; -import { IframeUtils } from 'vs/base/browser/iframe'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { isIOS } from 'vs/base/common/platform'; - -export interface IStandardMouseMoveEventData { - leftButton: boolean; - buttons: number; - posx: number; - posy: number; -} - -export interface IEventMerger { - (lastEvent: R | null, currentEvent: MouseEvent): R; -} - -export interface IMouseMoveCallback { - (mouseMoveData: R): void; -} - -export interface IOnStopCallback { - (browserEvent?: MouseEvent | KeyboardEvent): void; -} - -export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | null, currentEvent: MouseEvent): IStandardMouseMoveEventData { - let ev = new StandardMouseEvent(currentEvent); - ev.preventDefault(); - return { - leftButton: ev.leftButton, - buttons: ev.buttons, - posx: ev.posx, - posy: ev.posy - }; -} - -export class GlobalMouseMoveMonitor implements IDisposable { - - private readonly _hooks = new DisposableStore(); - private _mouseMoveEventMerger: IEventMerger | null = null; - private _mouseMoveCallback: IMouseMoveCallback | null = null; - private _onStopCallback: IOnStopCallback | null = null; - - public dispose(): void { - this.stopMonitoring(false); - this._hooks.dispose(); - } - - public stopMonitoring(invokeStopCallback: boolean, browserEvent?: MouseEvent | KeyboardEvent): void { - if (!this.isMonitoring()) { - // Not monitoring - return; - } - - // Unhook - this._hooks.clear(); - this._mouseMoveEventMerger = null; - this._mouseMoveCallback = null; - const onStopCallback = this._onStopCallback; - this._onStopCallback = null; - - if (invokeStopCallback && onStopCallback) { - onStopCallback(browserEvent); - } - } - - public isMonitoring(): boolean { - return !!this._mouseMoveEventMerger; - } - - public startMonitoring( - initialElement: HTMLElement, - initialButtons: number, - mouseMoveEventMerger: IEventMerger, - mouseMoveCallback: IMouseMoveCallback, - onStopCallback: IOnStopCallback - ): void { - if (this.isMonitoring()) { - // I am already hooked - return; - } - this._mouseMoveEventMerger = mouseMoveEventMerger; - this._mouseMoveCallback = mouseMoveCallback; - this._onStopCallback = onStopCallback; - - const windowChain = IframeUtils.getSameOriginWindowChain(); - const mouseMove = isIOS ? 'pointermove' : 'mousemove'; // Safari sends wrong event, workaround for #122653 - const mouseUp = 'mouseup'; - - const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document); - const shadowRoot = dom.getShadowRoot(initialElement); - if (shadowRoot) { - listenTo.unshift(shadowRoot); - } - - for (const element of listenTo) { - this._hooks.add(dom.addDisposableThrottledListener(element, mouseMove, - (data: R) => { - if (data.buttons !== initialButtons) { - // Buttons state has changed in the meantime - this.stopMonitoring(true); - return; - } - this._mouseMoveCallback!(data); - }, - (lastEvent: R | null, currentEvent) => this._mouseMoveEventMerger!(lastEvent, currentEvent as MouseEvent) - )); - this._hooks.add(dom.addDisposableListener(element, mouseUp, (e: MouseEvent) => this.stopMonitoring(true))); - } - - if (IframeUtils.hasDifferentOriginAncestor()) { - let lastSameOriginAncestor = windowChain[windowChain.length - 1]; - // We might miss a mouse up if it happens outside the iframe - // This one is for Chrome - this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseout', (browserEvent: MouseEvent) => { - let e = new StandardMouseEvent(browserEvent); - if (e.target.tagName.toLowerCase() === 'html') { - this.stopMonitoring(true); - } - })); - // This one is for FF - this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document, 'mouseover', (browserEvent: MouseEvent) => { - let e = new StandardMouseEvent(browserEvent); - if (e.target.tagName.toLowerCase() === 'html') { - this.stopMonitoring(true); - } - })); - // This one is for IE - this._hooks.add(dom.addDisposableListener(lastSameOriginAncestor.window.document.body, 'mouseleave', (browserEvent: MouseEvent) => { - this.stopMonitoring(true); - })); - } - } -} diff --git a/src/vs/base/browser/globalPointerMoveMonitor.ts b/src/vs/base/browser/globalPointerMoveMonitor.ts new file mode 100644 index 00000000000..a2ef86afd5b --- /dev/null +++ b/src/vs/base/browser/globalPointerMoveMonitor.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; + +export interface IPointerMoveEventData { + leftButton: boolean; + buttons: number; + pageX: number; + pageY: number; +} + +export interface IEventMerger { + (lastEvent: R | null, currentEvent: PointerEvent): R; +} + +export interface IPointerMoveCallback { + (pointerMoveData: R): void; +} + +export interface IOnStopCallback { + (browserEvent?: PointerEvent | KeyboardEvent): void; +} + +export function standardPointerMoveMerger(lastEvent: IPointerMoveEventData | null, currentEvent: PointerEvent): IPointerMoveEventData { + currentEvent.preventDefault(); + return { + leftButton: (currentEvent.button === 0), + buttons: currentEvent.buttons, + pageX: currentEvent.pageX, + pageY: currentEvent.pageY + }; +} + +export class GlobalPointerMoveMonitor implements IDisposable { + + private readonly _hooks = new DisposableStore(); + private _pointerMoveEventMerger: IEventMerger | null = null; + private _pointerMoveCallback: IPointerMoveCallback | null = null; + private _onStopCallback: IOnStopCallback | null = null; + + public dispose(): void { + this.stopMonitoring(false); + this._hooks.dispose(); + } + + public stopMonitoring(invokeStopCallback: boolean, browserEvent?: PointerEvent | KeyboardEvent): void { + if (!this.isMonitoring()) { + // Not monitoring + return; + } + + // Unhook + this._hooks.clear(); + this._pointerMoveEventMerger = null; + this._pointerMoveCallback = null; + const onStopCallback = this._onStopCallback; + this._onStopCallback = null; + + if (invokeStopCallback && onStopCallback) { + onStopCallback(browserEvent); + } + } + + public isMonitoring(): boolean { + return !!this._pointerMoveEventMerger; + } + + public startMonitoring( + initialElement: Element, + pointerId: number, + initialButtons: number, + pointerMoveEventMerger: IEventMerger, + pointerMoveCallback: IPointerMoveCallback, + onStopCallback: IOnStopCallback + ): void { + if (this.isMonitoring()) { + this.stopMonitoring(false); + } + this._pointerMoveEventMerger = pointerMoveEventMerger; + this._pointerMoveCallback = pointerMoveCallback; + this._onStopCallback = onStopCallback; + + initialElement.setPointerCapture(pointerId); + + this._hooks.add(toDisposable(() => { + initialElement.releasePointerCapture(pointerId); + })); + + this._hooks.add(dom.addDisposableThrottledListener( + initialElement, + dom.EventType.POINTER_MOVE, + (data: R) => { + if (data.buttons !== initialButtons) { + // Buttons state has changed in the meantime + this.stopMonitoring(true); + return; + } + this._pointerMoveCallback!(data); + }, + (lastEvent: R | null, currentEvent) => this._pointerMoveEventMerger!(lastEvent, currentEvent) + )); + + this._hooks.add(dom.addDisposableListener( + initialElement, + dom.EventType.POINTER_UP, + (e: PointerEvent) => this.stopMonitoring(true) + )); + } +} diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index 813218db45b..61108e58c67 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -326,27 +326,13 @@ function sanitizeRenderedMarkdown( } }); - // build an anchor to map URLs to - const anchor = document.createElement('a'); - - // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html - dompurify.addHook('afterSanitizeAttributes', (node) => { - // check all href/src attributes for validity - for (const attr of ['href', 'src']) { - if (node.hasAttribute(attr)) { - anchor.href = node.getAttribute(attr) as string; - if (!allowedSchemes.includes(anchor.protocol.replace(/:$/, ''))) { - node.removeAttribute(attr); - } - } - } - }); + const hook = DOM.hookDomPurifyHrefAndSrcSanitizer(allowedSchemes); try { return dompurify.sanitize(renderedMarkdown, { ...config, RETURN_TRUSTED_TYPE: true }); } finally { dompurify.removeHook('uponSanitizeAttribute'); - dompurify.removeHook('afterSanitizeAttributes'); + hook.dispose(); } } diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 7eba31492ba..02beee38fd2 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index e2511966a85..53d63ded4fa 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -7,7 +7,7 @@ import { $, addDisposableListener, clearNode, EventHelper, EventType, hide, isAn import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ButtonBar, ButtonWithDescription, IButtonStyles } from 'vs/base/browser/ui/button/button'; -import { ISimpleCheckboxStyles, SimpleCheckbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/toggle/toggle'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Action } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -46,7 +46,7 @@ export interface IDialogResult { readonly values?: string[]; } -export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles { +export interface IDialogStyles extends IButtonStyles, ICheckboxStyles { readonly dialogForeground?: Color; readonly dialogBackground?: Color; readonly dialogShadow?: Color; @@ -74,7 +74,7 @@ export class Dialog extends Disposable { private readonly messageDetailElement: HTMLElement; private readonly messageContainer: HTMLElement; private readonly iconElement: HTMLElement; - private readonly checkbox: SimpleCheckbox | undefined; + private readonly checkbox: Checkbox | undefined; private readonly toolbarContainer: HTMLElement; private buttonBar: ButtonBar | undefined; private styles: IDialogStyles | undefined; @@ -151,7 +151,7 @@ export class Dialog extends Disposable { if (this.options.checkboxLabel) { const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row')); - const checkbox = this.checkbox = this._register(new SimpleCheckbox(this.options.checkboxLabel, !!this.options.checkboxChecked)); + const checkbox = this.checkbox = this._register(new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked)); checkboxRowElement.appendChild(checkbox.domNode); diff --git a/src/vs/base/browser/ui/findinput/findInput.css b/src/vs/base/browser/ui/findinput/findInput.css index 4bb6b8cec63..3de2fd329ac 100644 --- a/src/vs/base/browser/ui/findinput/findInput.css +++ b/src/vs/base/browser/ui/findinput/findInput.css @@ -29,16 +29,21 @@ } /* Highlighting */ -.monaco-findInput.highlight-0 .controls { +.monaco-findInput.highlight-0 .controls, +.hc-light .monaco-findInput.highlight-0 .controls { animation: monaco-findInput-highlight-0 100ms linear 0s; } -.monaco-findInput.highlight-1 .controls { + +.monaco-findInput.highlight-1 .controls, +.hc-light .monaco-findInput.highlight-1 .controls { animation: monaco-findInput-highlight-1 100ms linear 0s; } + .hc-black .monaco-findInput.highlight-0 .controls, .vs-dark .monaco-findInput.highlight-0 .controls { animation: monaco-findInput-highlight-dark-0 100ms linear 0s; } + .hc-black .monaco-findInput.highlight-1 .controls, .vs-dark .monaco-findInput.highlight-1 .controls { animation: monaco-findInput-highlight-dark-1 100ms linear 0s; @@ -62,4 +67,4 @@ 0% { background: rgba(255, 255, 255, 0.44); } /* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/ 99% { background: transparent; } -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 3879b55ec5f..bc23ea46c6b 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -6,9 +6,9 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox'; +import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { CaseSensitiveCheckbox, RegexCheckbox, WholeWordsCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes'; +import { CaseSensitiveToggle, RegexToggle, WholeWordsToggle } from 'vs/base/browser/ui/findinput/findInputToggles'; import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Color } from 'vs/base/common/color'; @@ -71,9 +71,9 @@ export class FindInput extends Widget { protected inputValidationErrorForeground?: Color; protected controls: HTMLDivElement; - protected regex: RegexCheckbox; - protected wholeWords: WholeWordsCheckbox; - protected caseSensitive: CaseSensitiveCheckbox; + protected regex: RegexToggle; + protected wholeWords: WholeWordsToggle; + protected caseSensitive: CaseSensitiveToggle; public domNode: HTMLElement; public inputBox: HistoryInputBox; @@ -158,7 +158,7 @@ export class FindInput extends Widget { flexibleMaxHeight })); - this.regex = this._register(new RegexCheckbox({ + this.regex = this._register(new RegexToggle({ appendTitle: appendRegexLabel, isChecked: false, inputActiveOptionBorder: this.inputActiveOptionBorder, @@ -176,7 +176,7 @@ export class FindInput extends Widget { this._onRegexKeyDown.fire(e); })); - this.wholeWords = this._register(new WholeWordsCheckbox({ + this.wholeWords = this._register(new WholeWordsToggle({ appendTitle: appendWholeWordsLabel, isChecked: false, inputActiveOptionBorder: this.inputActiveOptionBorder, @@ -191,7 +191,7 @@ export class FindInput extends Widget { this.validate(); })); - this.caseSensitive = this._register(new CaseSensitiveCheckbox({ + this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: appendCaseSensitiveLabel, isChecked: false, inputActiveOptionBorder: this.inputActiveOptionBorder, @@ -349,14 +349,14 @@ export class FindInput extends Widget { protected applyStyles(): void { if (this.domNode) { - const checkBoxStyles: ICheckboxStyles = { + const toggleStyles: IToggleStyles = { inputActiveOptionBorder: this.inputActiveOptionBorder, inputActiveOptionForeground: this.inputActiveOptionForeground, inputActiveOptionBackground: this.inputActiveOptionBackground, }; - this.regex.style(checkBoxStyles); - this.wholeWords.style(checkBoxStyles); - this.caseSensitive.style(checkBoxStyles); + this.regex.style(toggleStyles); + this.wholeWords.style(toggleStyles); + this.caseSensitive.style(toggleStyles); const inputBoxStyles: IInputBoxStyles = { inputBackground: this.inputBackground, diff --git a/src/vs/base/browser/ui/findinput/findInputCheckboxes.ts b/src/vs/base/browser/ui/findinput/findInputToggles.ts similarity index 63% rename from src/vs/base/browser/ui/findinput/findInputCheckboxes.ts rename to src/vs/base/browser/ui/findinput/findInputToggles.ts index 1ebaa956c91..8f6f30dda44 100644 --- a/src/vs/base/browser/ui/findinput/findInputCheckboxes.ts +++ b/src/vs/base/browser/ui/findinput/findInputToggles.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import * as nls from 'vs/nls'; -export interface IFindInputCheckboxOpts { +export interface IFindInputToggleOpts { readonly appendTitle: string; readonly isChecked: boolean; readonly inputActiveOptionBorder?: Color; @@ -16,15 +16,15 @@ export interface IFindInputCheckboxOpts { readonly inputActiveOptionBackground?: Color; } -const NLS_CASE_SENSITIVE_CHECKBOX_LABEL = nls.localize('caseDescription', "Match Case"); -const NLS_WHOLE_WORD_CHECKBOX_LABEL = nls.localize('wordsDescription', "Match Whole Word"); -const NLS_REGEX_CHECKBOX_LABEL = nls.localize('regexDescription', "Use Regular Expression"); +const NLS_CASE_SENSITIVE_TOGGLE_LABEL = nls.localize('caseDescription', "Match Case"); +const NLS_WHOLE_WORD_TOGGLE_LABEL = nls.localize('wordsDescription', "Match Whole Word"); +const NLS_REGEX_TOGGLE_LABEL = nls.localize('regexDescription', "Use Regular Expression"); -export class CaseSensitiveCheckbox extends Checkbox { - constructor(opts: IFindInputCheckboxOpts) { +export class CaseSensitiveToggle extends Toggle { + constructor(opts: IFindInputToggleOpts) { super({ icon: Codicon.caseSensitive, - title: NLS_CASE_SENSITIVE_CHECKBOX_LABEL + opts.appendTitle, + title: NLS_CASE_SENSITIVE_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, @@ -33,11 +33,11 @@ export class CaseSensitiveCheckbox extends Checkbox { } } -export class WholeWordsCheckbox extends Checkbox { - constructor(opts: IFindInputCheckboxOpts) { +export class WholeWordsToggle extends Toggle { + constructor(opts: IFindInputToggleOpts) { super({ icon: Codicon.wholeWord, - title: NLS_WHOLE_WORD_CHECKBOX_LABEL + opts.appendTitle, + title: NLS_WHOLE_WORD_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, @@ -46,11 +46,11 @@ export class WholeWordsCheckbox extends Checkbox { } } -export class RegexCheckbox extends Checkbox { - constructor(opts: IFindInputCheckboxOpts) { +export class RegexToggle extends Toggle { + constructor(opts: IFindInputToggleOpts) { super({ icon: Codicon.regex, - title: NLS_REGEX_CHECKBOX_LABEL + opts.appendTitle, + title: NLS_REGEX_TOGGLE_LABEL + opts.appendTitle, isChecked: opts.isChecked, inputActiveOptionBorder: opts.inputActiveOptionBorder, inputActiveOptionForeground: opts.inputActiveOptionForeground, diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index f694491f29c..d2008722b71 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -6,9 +6,9 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Checkbox, ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle, IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { IFindInputCheckboxOpts } from 'vs/base/browser/ui/findinput/findInputCheckboxes'; +import { IFindInputToggleOpts } from 'vs/base/browser/ui/findinput/findInputToggles'; import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Codicon } from 'vs/base/common/codicons'; @@ -40,10 +40,10 @@ export interface IReplaceInputStyles extends IInputBoxStyles { } const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); -const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseCheckbox', "Preserve Case"); +const NLS_PRESERVE_CASE_LABEL = nls.localize('label.preserveCaseToggle', "Preserve Case"); -export class PreserveCaseCheckbox extends Checkbox { - constructor(opts: IFindInputCheckboxOpts) { +export class PreserveCaseToggle extends Toggle { + constructor(opts: IFindInputToggleOpts) { super({ // TODO: does this need its own icon? icon: Codicon.preserveCase, @@ -83,7 +83,7 @@ export class ReplaceInput extends Widget { private inputValidationErrorBackground?: Color; private inputValidationErrorForeground?: Color; - private preserveCase: PreserveCaseCheckbox; + private preserveCase: PreserveCaseToggle; private cachedOptionsWidth: number = 0; public domNode: HTMLElement; public inputBox: HistoryInputBox; @@ -164,7 +164,7 @@ export class ReplaceInput extends Widget { flexibleMaxHeight })); - this.preserveCase = this._register(new PreserveCaseCheckbox({ + this.preserveCase = this._register(new PreserveCaseToggle({ appendTitle: appendPreserveCaseLabel, isChecked: false, inputActiveOptionBorder: this.inputActiveOptionBorder, @@ -302,12 +302,12 @@ export class ReplaceInput extends Widget { protected applyStyles(): void { if (this.domNode) { - const checkBoxStyles: ICheckboxStyles = { + const toggleStyles: IToggleStyles = { inputActiveOptionBorder: this.inputActiveOptionBorder, inputActiveOptionForeground: this.inputActiveOptionForeground, inputActiveOptionBackground: this.inputActiveOptionBackground, }; - this.preserveCase.style(checkBoxStyles); + this.preserveCase.style(toggleStyles); const inputBoxStyles: IInputBoxStyles = { inputBackground: this.inputBackground, diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index d7ae40774b9..3fcc9896c7f 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -263,6 +263,7 @@ export class ListView implements ISpliceable, IDisposable { get onDidScroll(): Event { return this.scrollableElement.onScroll; } get onWillScroll(): Event { return this.scrollableElement.onWillScroll; } get containerDomNode(): HTMLElement { return this.rowsContainer; } + get scrollableElementDomNode(): HTMLElement { return this.scrollableElement.getDomNode(); } private _horizontalScrolling: boolean = false; private get horizontalScrolling(): boolean { return this._horizontalScrolling; } diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index c3852e44786..533ca04a116 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -802,8 +802,10 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem { })); this._register(this.parentData.parent.onScroll(() => { - this.parentData.parent.focus(false); - this.cleanupExistingSubmenu(false); + if (this.parentData.submenu === this.mysubmenu) { + this.parentData.parent.focus(false); + this.cleanupExistingSubmenu(true); + } })); } @@ -1242,11 +1244,13 @@ ${formatRule(Codicon.menuSubmenu)} /* High Contrast Theming */ -:host-context(.hc-black) .context-view.monaco-menu-container { +:host-context(.hc-black) .context-view.monaco-menu-container, +:host-context(.hc-light) .context-view.monaco-menu-container { box-shadow: none; } -:host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused { +:host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused, +:host-context(.hc-light) .monaco-menu .monaco-action-bar.vertical .action-item.focused { background: none; } diff --git a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts index 7d47b1ff424..fcb825d2aad 100644 --- a/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts @@ -5,8 +5,8 @@ import * as dom from 'vs/base/browser/dom'; import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode'; -import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; -import { IMouseEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent'; +import { GlobalPointerMoveMonitor, IPointerMoveEventData, standardPointerMoveMerger } from 'vs/base/browser/globalPointerMoveMonitor'; +import { StandardWheelEvent } from 'vs/base/browser/mouseEvent'; import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController'; @@ -17,12 +17,12 @@ import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/com /** * The orthogonal distance to the slider at which dragging "resets". This implements "snapping" */ -const MOUSE_DRAG_RESET_DISTANCE = 140; +const POINTER_DRAG_RESET_DISTANCE = 140; -export interface ISimplifiedMouseEvent { +export interface ISimplifiedPointerEvent { buttons: number; - posx: number; - posy: number; + pageX: number; + pageY: number; } export interface ScrollbarHost { @@ -49,7 +49,7 @@ export abstract class AbstractScrollbar extends Widget { private _lazyRender: boolean; protected _scrollbarState: ScrollbarState; protected _visibilityController: ScrollbarVisibilityController; - private _mouseMoveMonitor: GlobalMouseMoveMonitor; + private _pointerMoveMonitor: GlobalPointerMoveMonitor; public domNode: FastDomNode; public slider!: FastDomNode; @@ -65,7 +65,7 @@ export abstract class AbstractScrollbar extends Widget { this._scrollbarState = opts.scrollbarState; this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName)); this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()); - this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); + this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor()); this._shouldRender = true; this.domNode = createFastDomNode(document.createElement('div')); this.domNode.setAttribute('role', 'presentation'); @@ -74,7 +74,7 @@ export abstract class AbstractScrollbar extends Widget { this._visibilityController.setDomNode(this.domNode); this.domNode.setPosition('absolute'); - this.onmousedown(this.domNode.domNode, (e) => this._domNodeMouseDown(e)); + this._register(dom.addDisposableListener(this.domNode.domNode, dom.EventType.POINTER_DOWN, (e: PointerEvent) => this._domNodePointerDown(e))); } // ----------------- creation @@ -108,12 +108,16 @@ export abstract class AbstractScrollbar extends Widget { this.domNode.domNode.appendChild(this.slider.domNode); - this.onmousedown(this.slider.domNode, (e) => { - if (e.leftButton) { - e.preventDefault(); - this._sliderMouseDown(e, () => { /*nothing to do*/ }); + this._register(dom.addDisposableListener( + this.slider.domNode, + dom.EventType.POINTER_DOWN, + (e: PointerEvent) => { + if (e.button === 0) { + e.preventDefault(); + this._sliderPointerDown(e); + } } - }); + )); this.onclick(this.slider.domNode, e => { if (e.leftButton) { @@ -178,83 +182,87 @@ export abstract class AbstractScrollbar extends Widget { } // ----------------- DOM events - private _domNodeMouseDown(e: IMouseEvent): void { + private _domNodePointerDown(e: PointerEvent): void { if (e.target !== this.domNode.domNode) { return; } - this._onMouseDown(e); + this._onPointerDown(e); } - public delegateMouseDown(e: IMouseEvent): void { + public delegatePointerDown(e: PointerEvent): void { const domTop = this.domNode.domNode.getClientRects()[0].top; const sliderStart = domTop + this._scrollbarState.getSliderPosition(); const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize(); - const mousePos = this._sliderMousePosition(e); - if (sliderStart <= mousePos && mousePos <= sliderStop) { - // Act as if it was a mouse down on the slider - if (e.leftButton) { + const pointerPos = this._sliderPointerPosition(e); + if (sliderStart <= pointerPos && pointerPos <= sliderStop) { + // Act as if it was a pointer down on the slider + if (e.button === 0) { e.preventDefault(); - this._sliderMouseDown(e, () => { /*nothing to do*/ }); + this._sliderPointerDown(e); } } else { - // Act as if it was a mouse down on the scrollbar - this._onMouseDown(e); + // Act as if it was a pointer down on the scrollbar + this._onPointerDown(e); } } - private _onMouseDown(e: IMouseEvent): void { + private _onPointerDown(e: PointerEvent): void { let offsetX: number; let offsetY: number; - if (e.target === this.domNode.domNode && typeof e.browserEvent.offsetX === 'number' && typeof e.browserEvent.offsetY === 'number') { - offsetX = e.browserEvent.offsetX; - offsetY = e.browserEvent.offsetY; + if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') { + offsetX = e.offsetX; + offsetY = e.offsetY; } else { const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode); - offsetX = e.posx - domNodePosition.left; - offsetY = e.posy - domNodePosition.top; + offsetX = e.pageX - domNodePosition.left; + offsetY = e.pageY - domNodePosition.top; } - const offset = this._mouseDownRelativePosition(offsetX, offsetY); + const offset = this._pointerDownRelativePosition(offsetX, offsetY); this._setDesiredScrollPositionNow( this._scrollByPage ? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset) : this._scrollbarState.getDesiredScrollPositionFromOffset(offset) ); - if (e.leftButton) { + if (e.button === 0) { + // left button e.preventDefault(); - this._sliderMouseDown(e, () => { /*nothing to do*/ }); + this._sliderPointerDown(e); } } - private _sliderMouseDown(e: IMouseEvent, onDragFinished: () => void): void { - const initialMousePosition = this._sliderMousePosition(e); - const initialMouseOrthogonalPosition = this._sliderOrthogonalMousePosition(e); + private _sliderPointerDown(e: PointerEvent): void { + if (!e.target || !(e.target instanceof Element)) { + return; + } + const initialPointerPosition = this._sliderPointerPosition(e); + const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e); const initialScrollbarState = this._scrollbarState.clone(); this.slider.toggleClassName('active', true); - this._mouseMoveMonitor.startMonitoring( + this._pointerMoveMonitor.startMonitoring( e.target, + e.pointerId, e.buttons, - standardMouseMoveMerger, - (mouseMoveData: IStandardMouseMoveEventData) => { - const mouseOrthogonalPosition = this._sliderOrthogonalMousePosition(mouseMoveData); - const mouseOrthogonalDelta = Math.abs(mouseOrthogonalPosition - initialMouseOrthogonalPosition); + standardPointerMoveMerger, + (pointerMoveData: IPointerMoveEventData) => { + const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData); + const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition); - if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) { - // The mouse has wondered away from the scrollbar => reset dragging + if (platform.isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) { + // The pointer has wondered away from the scrollbar => reset dragging this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition()); return; } - const mousePosition = this._sliderMousePosition(mouseMoveData); - const mouseDelta = mousePosition - initialMousePosition; - this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(mouseDelta)); + const pointerPosition = this._sliderPointerPosition(pointerMoveData); + const pointerDelta = pointerPosition - initialPointerPosition; + this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta)); }, () => { this.slider.toggleClassName('active', false); this._host.onDragEnd(); - onDragFinished(); } ); @@ -287,9 +295,9 @@ export abstract class AbstractScrollbar extends Widget { protected abstract _renderDomNode(largeSize: number, smallSize: number): void; protected abstract _updateSlider(sliderSize: number, sliderPosition: number): void; - protected abstract _mouseDownRelativePosition(offsetX: number, offsetY: number): number; - protected abstract _sliderMousePosition(e: ISimplifiedMouseEvent): number; - protected abstract _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number; + protected abstract _pointerDownRelativePosition(offsetX: number, offsetY: number): number; + protected abstract _sliderPointerPosition(e: ISimplifiedPointerEvent): number; + protected abstract _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number; protected abstract _updateScrollbarSize(size: number): void; public abstract writeScrollPosition(target: INewScrollPosition, scrollPosition: number): void; diff --git a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts index 2816a12c8e4..9672118552d 100644 --- a/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { StandardWheelEvent } from 'vs/base/browser/mouseEvent'; -import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; +import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; @@ -87,16 +87,16 @@ export class HorizontalScrollbar extends AbstractScrollbar { return this._shouldRender; } - protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number { + protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number { return offsetX; } - protected _sliderMousePosition(e: ISimplifiedMouseEvent): number { - return e.posx; + protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number { + return e.pageX; } - protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number { - return e.posy; + protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number { + return e.pageY; } protected _updateScrollbarSize(size: number): void { diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index aa6be1c229b..f83c34fe519 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -263,11 +263,11 @@ export abstract class AbstractScrollableElement extends Widget { } /** - * Delegate a mouse down event to the vertical scrollbar. + * Delegate a pointer down event to the vertical scrollbar. * This is to help with clicking somewhere else and having the scrollbar react. */ - public delegateVerticalScrollbarMouseDown(browserEvent: IMouseEvent): void { - this._verticalScrollbar.delegateMouseDown(browserEvent); + public delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void { + this._verticalScrollbar.delegatePointerDown(browserEvent); } public getScrollDimensions(): IScrollDimensions { diff --git a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts index 03601e9e34c..acb27a85b5b 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; +import { GlobalPointerMoveMonitor, standardPointerMoveMerger } from 'vs/base/browser/globalPointerMoveMonitor'; import { Widget } from 'vs/base/browser/ui/widget'; import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; +import * as dom from 'vs/base/browser/dom'; /** * The arrow image size. @@ -33,9 +33,9 @@ export class ScrollbarArrow extends Widget { private _onActivate: () => void; public bgDomNode: HTMLElement; public domNode: HTMLElement; - private _mousedownRepeatTimer: IntervalTimer; - private _mousedownScheduleRepeatTimer: TimeoutTimer; - private _mouseMoveMonitor: GlobalMouseMoveMonitor; + private _pointerdownRepeatTimer: IntervalTimer; + private _pointerdownScheduleRepeatTimer: TimeoutTimer; + private _pointerMoveMonitor: GlobalPointerMoveMonitor; constructor(opts: ScrollbarArrowOptions) { super(); @@ -79,33 +79,35 @@ export class ScrollbarArrow extends Widget { this.domNode.style.right = opts.right + 'px'; } - this._mouseMoveMonitor = this._register(new GlobalMouseMoveMonitor()); - this.onmousedown(this.bgDomNode, (e) => this._arrowMouseDown(e)); - this.onmousedown(this.domNode, (e) => this._arrowMouseDown(e)); + this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor()); + this._register(dom.addStandardDisposableListener(this.bgDomNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e))); + this._register(dom.addStandardDisposableListener(this.domNode, dom.EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e))); - this._mousedownRepeatTimer = this._register(new IntervalTimer()); - this._mousedownScheduleRepeatTimer = this._register(new TimeoutTimer()); + this._pointerdownRepeatTimer = this._register(new IntervalTimer()); + this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer()); } - private _arrowMouseDown(e: IMouseEvent): void { + private _arrowPointerDown(e: PointerEvent): void { + if (!e.target || !(e.target instanceof Element)) { + return; + } const scheduleRepeater = () => { - this._mousedownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24); + this._pointerdownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24); }; this._onActivate(); - this._mousedownRepeatTimer.cancel(); - this._mousedownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200); + this._pointerdownRepeatTimer.cancel(); + this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200); - this._mouseMoveMonitor.startMonitoring( + this._pointerMoveMonitor.startMonitoring( e.target, + e.pointerId, e.buttons, - standardMouseMoveMerger, - (mouseMoveData: IStandardMouseMoveEventData) => { - /* Intentional empty */ - }, + standardPointerMoveMerger, + (pointerMoveData) => { /* Intentional empty */ }, () => { - this._mousedownRepeatTimer.cancel(); - this._mousedownScheduleRepeatTimer.cancel(); + this._pointerdownRepeatTimer.cancel(); + this._pointerdownScheduleRepeatTimer.cancel(); } ); diff --git a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts index 69f5984704d..672bde8e017 100644 --- a/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +++ b/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { StandardWheelEvent } from 'vs/base/browser/mouseEvent'; -import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; +import { AbstractScrollbar, ISimplifiedPointerEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar'; import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow'; import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState'; @@ -87,16 +87,16 @@ export class VerticalScrollbar extends AbstractScrollbar { return this._shouldRender; } - protected _mouseDownRelativePosition(offsetX: number, offsetY: number): number { + protected _pointerDownRelativePosition(offsetX: number, offsetY: number): number { return offsetY; } - protected _sliderMousePosition(e: ISimplifiedMouseEvent): number { - return e.posy; + protected _sliderPointerPosition(e: ISimplifiedPointerEvent): number { + return e.pageY; } - protected _sliderOrthogonalMousePosition(e: ISimplifiedMouseEvent): number { - return e.posx; + protected _sliderOrthogonalPointerPosition(e: ISimplifiedPointerEvent): number { + return e.pageX; } protected _updateScrollbarSize(size: number): void { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index f385bf29df6..664d38f1709 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -9,7 +9,8 @@ --dropdown-padding-bottom: 1px; } -.hc-black .monaco-select-box-dropdown-padding { +.hc-black .monaco-select-box-dropdown-padding, +.hc-light .monaco-select-box-dropdown-padding { --dropdown-padding-top: 3px; --dropdown-padding-bottom: 4px; } diff --git a/src/vs/base/browser/ui/splitview/paneview.css b/src/vs/base/browser/ui/splitview/paneview.css index 7dc22bed9f2..07981a17842 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -110,6 +110,10 @@ transition-timing-function: ease-out; } +.reduce-motion .monaco-pane-view .split-view-view { + transition-duration: 0s !important; +} + .monaco-pane-view.animated.vertical .split-view-view { transition-property: height; } diff --git a/src/vs/base/browser/ui/checkbox/checkbox.css b/src/vs/base/browser/ui/toggle/toggle.css similarity index 72% rename from src/vs/base/browser/ui/checkbox/checkbox.css rename to src/vs/base/browser/ui/toggle/toggle.css index 706c9585e40..9c84fed3cfc 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.css +++ b/src/vs/base/browser/ui/toggle/toggle.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-custom-checkbox { +.monaco-custom-toggle { margin-left: 2px; float: left; cursor: pointer; @@ -19,23 +19,26 @@ -ms-user-select: none; } -.monaco-custom-checkbox:hover { +.monaco-custom-toggle:hover { background-color: var(--vscode-inputOption-hoverBackground); } -.hc-black .monaco-custom-checkbox:hover { +.hc-black .monaco-custom-toggle:hover, +.hc-light .monaco-custom-toggle:hover { border: 1px dashed var(--vscode-focusBorder); } -.hc-black .monaco-custom-checkbox { +.hc-black .monaco-custom-toggle, +.hc-light .monaco-custom-toggle { background: none; } -.hc-black .monaco-custom-checkbox:hover { +.hc-black .monaco-custom-toggle:hover, +.hc-light .monaco-custom-toggle:hover { background: none; } -.monaco-custom-checkbox.monaco-simple-checkbox { +.monaco-custom-toggle.monaco-checkbox { height: 18px; width: 18px; border: 1px solid transparent; @@ -48,6 +51,6 @@ } /* hide check when unchecked */ -.monaco-custom-checkbox.monaco-simple-checkbox:not(.checked)::before { +.monaco-custom-toggle.monaco-checkbox:not(.checked)::before { visibility: hidden; } diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/toggle/toggle.ts similarity index 84% rename from src/vs/base/browser/ui/checkbox/checkbox.ts rename to src/vs/base/browser/ui/toggle/toggle.ts index 33ad43fc5bb..dafff0bf5ff 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -11,9 +11,9 @@ import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import 'vs/css!./checkbox'; +import 'vs/css!./toggle'; -export interface ICheckboxOpts extends ICheckboxStyles { +export interface IToggleOpts extends IToggleStyles { readonly actionClassName?: string; readonly icon?: CSSIcon; readonly title: string; @@ -21,13 +21,13 @@ export interface ICheckboxOpts extends ICheckboxStyles { readonly notFocusable?: boolean; } -export interface ICheckboxStyles { +export interface IToggleStyles { inputActiveOptionBorder?: Color; inputActiveOptionForeground?: Color; inputActiveOptionBackground?: Color; } -export interface ISimpleCheckboxStyles { +export interface ICheckboxStyles { checkboxBackground?: Color; checkboxBorder?: Color; checkboxForeground?: Color; @@ -39,57 +39,57 @@ const defaultOpts = { inputActiveOptionBackground: Color.fromHex('#0E639C50') }; -export class CheckboxActionViewItem extends BaseActionViewItem { +export class ToggleActionViewItem extends BaseActionViewItem { - protected readonly checkbox: Checkbox; + protected readonly toggle: Toggle; constructor(context: any, action: IAction, options: IActionViewItemOptions | undefined) { super(context, action, options); - this.checkbox = this._register(new Checkbox({ + this.toggle = this._register(new Toggle({ actionClassName: this._action.class, isChecked: !!this._action.checked, title: (this.options).keybinding ? `${this._action.label} (${(this.options).keybinding})` : this._action.label, notFocusable: true })); - this._register(this.checkbox.onChange(() => this._action.checked = !!this.checkbox && this.checkbox.checked)); + this._register(this.toggle.onChange(() => this._action.checked = !!this.toggle && this.toggle.checked)); } override render(container: HTMLElement): void { this.element = container; - this.element.appendChild(this.checkbox.domNode); + this.element.appendChild(this.toggle.domNode); } override updateEnabled(): void { - if (this.checkbox) { + if (this.toggle) { if (this.isEnabled()) { - this.checkbox.enable(); + this.toggle.enable(); } else { - this.checkbox.disable(); + this.toggle.disable(); } } } override updateChecked(): void { - this.checkbox.checked = !!this._action.checked; + this.toggle.checked = !!this._action.checked; } override focus(): void { - this.checkbox.domNode.tabIndex = 0; - this.checkbox.focus(); + this.toggle.domNode.tabIndex = 0; + this.toggle.focus(); } override blur(): void { - this.checkbox.domNode.tabIndex = -1; - this.checkbox.domNode.blur(); + this.toggle.domNode.tabIndex = -1; + this.toggle.domNode.blur(); } override setFocusable(focusable: boolean): void { - this.checkbox.domNode.tabIndex = focusable ? 0 : -1; + this.toggle.domNode.tabIndex = focusable ? 0 : -1; } } -export class Checkbox extends Widget { +export class Toggle extends Widget { private readonly _onChange = this._register(new Emitter()); readonly onChange: Event = this._onChange.event; @@ -97,18 +97,18 @@ export class Checkbox extends Widget { private readonly _onKeyDown = this._register(new Emitter()); readonly onKeyDown: Event = this._onKeyDown.event; - private readonly _opts: ICheckboxOpts; + private readonly _opts: IToggleOpts; readonly domNode: HTMLElement; private _checked: boolean; - constructor(opts: ICheckboxOpts) { + constructor(opts: IToggleOpts) { super(); this._opts = { ...defaultOpts, ...opts }; this._checked = this._opts.isChecked; - const classes = ['monaco-custom-checkbox']; + const classes = ['monaco-custom-toggle']; if (this._opts.icon) { classes.push(...CSSIcon.asClassNameArray(this._opts.icon)); } @@ -178,7 +178,7 @@ export class Checkbox extends Widget { return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */; } - style(styles: ICheckboxStyles): void { + style(styles: IToggleStyles): void { if (styles.inputActiveOptionBorder) { this._opts.inputActiveOptionBorder = styles.inputActiveOptionBorder; } @@ -213,16 +213,16 @@ export class Checkbox extends Widget { } } -export class SimpleCheckbox extends Widget { - private checkbox: Checkbox; - private styles: ISimpleCheckboxStyles; +export class Checkbox extends Widget { + private checkbox: Toggle; + private styles: ICheckboxStyles; readonly domNode: HTMLElement; constructor(private title: string, private isChecked: boolean) { super(); - this.checkbox = new Checkbox({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-simple-checkbox' }); + this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox' }); this.domNode = this.checkbox.domNode; @@ -251,7 +251,7 @@ export class SimpleCheckbox extends Widget { return this.domNode === document.activeElement; } - style(styles: ISimpleCheckboxStyles): void { + style(styles: ICheckboxStyles): void { this.styles = styles; this.applyStyles(); diff --git a/src/vs/base/buildfile.js b/src/vs/base/buildfile.js deleted file mode 100644 index b2240ca2f1c..00000000000 --- a/src/vs/base/buildfile.js +++ /dev/null @@ -1,33 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -/** - * @param {string} name - * @param {string[]} exclude - */ -function createModuleDescription(name, exclude) { - - let excludes = ['vs/css', 'vs/nls']; - if (Array.isArray(exclude) && exclude.length > 0) { - excludes = excludes.concat(exclude); - } - - return { - name: name, - include: [], - exclude: excludes - }; -} - -/** - * @param {string} name - */ -function createEditorWorkerModuleDescription(name) { - return createModuleDescription(name, ['vs/base/common/worker/simpleWorker', 'vs/editor/common/services/editorSimpleWorker']); -} - -exports.createModuleDescription = createModuleDescription; -exports.createEditorWorkerModuleDescription = createEditorWorkerModuleDescription; diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 8120619b243..d10039a556f 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -380,6 +380,12 @@ export function firstOrDefault(array: ReadonlyArray, notFoun return array.length > 0 ? array[0] : notFoundValue; } +export function lastOrDefault(array: ReadonlyArray, notFoundValue: NotFound): T | NotFound; +export function lastOrDefault(array: ReadonlyArray): T | undefined; +export function lastOrDefault(array: ReadonlyArray, notFoundValue?: NotFound): T | NotFound | undefined { + return array.length > 0 ? array[array.length - 1] : notFoundValue; +} + export function commonPrefixLength(one: ReadonlyArray, other: ReadonlyArray, equals: (a: T, b: T) => boolean = (a, b) => a === b): number { let result = 0; @@ -390,6 +396,9 @@ export function commonPrefixLength(one: ReadonlyArray, other: ReadonlyArra return result; } +/** + * @deprecated Use `[].flat()` + */ export function flatten(arr: T[][]): T[] { return ([]).concat(...arr); } @@ -433,6 +442,8 @@ export function index(array: ReadonlyArray, indexer: (t: T) => string, /** * Inserts an element into an array. Returns a function which, when * called, will remove that element from the array. + * + * @deprecated In almost all cases, use a `Set` instead. */ export function insert(array: T[], element: T): () => void { array.push(element); @@ -442,6 +453,8 @@ export function insert(array: T[], element: T): () => void { /** * Removes an element from an array if it can be found. + * + * @deprecated In almost all cases, use a `Set` instead. */ export function remove(array: T[], element: T): T | undefined { const index = array.indexOf(element); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 0d170c9bd00..da4eb72af0a 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -597,17 +597,25 @@ export class Limiter implements ILimiter{ private runningPromises: number; private maxDegreeOfParalellism: number; private outstandingPromises: ILimitedTaskFactory[]; - private readonly _onFinished: Emitter; + private readonly _onDrained: Emitter; constructor(maxDegreeOfParalellism: number) { this.maxDegreeOfParalellism = maxDegreeOfParalellism; this.outstandingPromises = []; this.runningPromises = 0; - this._onFinished = new Emitter(); + this._onDrained = new Emitter(); } - get onFinished(): Event { - return this._onFinished.event; + /** + * An event that fires when every promise in the queue + * has started to execute. In other words: no work is + * pending to be scheduled. + * + * This is NOT an event that signals when all promises + * have finished though. + */ + get onDrained(): Event { + return this._onDrained.event; } get size(): number { @@ -641,12 +649,12 @@ export class Limiter implements ILimiter{ if (this.outstandingPromises.length > 0) { this.consume(); } else { - this._onFinished.fire(); + this._onDrained.fire(); } } dispose(): void { - this._onFinished.dispose(); + this._onDrained.dispose(); } } @@ -697,10 +705,10 @@ export class ResourceQueue implements IDisposable { let queue = this.queues.get(key); if (!queue) { queue = new Queue(); - Event.once(queue.onFinished)(() => { + Event.once(queue.onDrained)(() => { queue?.dispose(); this.queues.delete(key); - this.onDidQueueFinish(); + this.onDidQueueDrain(); }); this.queues.set(key, queue); @@ -709,7 +717,7 @@ export class ResourceQueue implements IDisposable { return queue; } - private onDidQueueFinish(): void { + private onDidQueueDrain(): void { if (!this.isDrained()) { return; // not done yet } diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index cb8c8c6da2d..9cc02257cab 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -46,12 +46,12 @@ export namespace CancellationToken { } - export const None: CancellationToken = Object.freeze({ + export const None = Object.freeze({ isCancellationRequested: false, onCancellationRequested: Event.None }); - export const Cancelled: CancellationToken = Object.freeze({ + export const Cancelled = Object.freeze({ isCancellationRequested: true, onCancellationRequested: shortcutEvent }); diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index 20186b38f07..08528474361 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -233,5 +233,18 @@ export class ExpectedError extends Error { * Error that when thrown won't be logged in telemetry as an unhandled error. */ export class ErrorNoTelemetry extends Error { + + public static fromError(err: any): ErrorNoTelemetry { + if (err && err instanceof Error) { + const result = new ErrorNoTelemetry(); + result.name = err.name; + result.message = err.message; + result.stack = err.stack; + return result; + } + + return new ErrorNoTelemetry(err); + } + readonly logTelemetry = false; } diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index aa074b3ea9a..53615f750c5 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -349,7 +349,7 @@ export interface IItemScore { descriptionMatch?: IMatch[]; } -const NO_ITEM_SCORE: IItemScore = Object.freeze({ score: 0 }); +const NO_ITEM_SCORE = Object.freeze({ score: 0 }); export interface IItemAccessor { diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index e720494366c..fd691e9f568 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -11,10 +11,6 @@ import { basename, extname, posix, sep } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; -export interface IExpression { - [pattern: string]: boolean | SiblingClause; -} - export interface IRelativePattern { /** @@ -32,11 +28,15 @@ export interface IRelativePattern { readonly pattern: string; } +export interface IExpression { + [pattern: string]: boolean | SiblingClause; +} + export function getEmptyExpression(): IExpression { return Object.create(null); } -export interface SiblingClause { +interface SiblingClause { when: string; } @@ -47,17 +47,18 @@ const PATH_REGEX = '[/\\\\]'; // any slash or backslash const NO_PATH_REGEX = '[^/\\\\]'; // any non-slash and non-backslash const ALL_FORWARD_SLASHES = /\//g; -function starsToRegExp(starCount: number): string { +function starsToRegExp(starCount: number, isLastPattern?: boolean): string { switch (starCount) { case 0: return ''; case 1: return `${NO_PATH_REGEX}*?`; // 1 star matches any number of characters except path separator (/ and \) - non greedy (?) default: - // Matches: (Path Sep OR Path Val followed by Path Sep OR Path Sep followed by Path Val) 0-many times + // Matches: (Path Sep OR Path Val followed by Path Sep) 0-many times except when it's the last pattern + // in which case also matches (Path Sep followed by Path Val) // Group is non capturing because we don't need to capture at all (?:...) // Overall we use non-greedy matching because it could be that we match too much - return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}|${PATH_REGEX}${NO_PATH_REGEX}+)*?`; + return `(?:${PATH_REGEX}|${NO_PATH_REGEX}+${PATH_REGEX}${isLastPattern ? `|${PATH_REGEX}${NO_PATH_REGEX}+` : ''})*?`; } } @@ -118,7 +119,7 @@ function parseRegExp(pattern: string): string { const segments = splitGlobAware(pattern, GLOB_SPLIT); // Special case where we only have globstars - if (segments.every(s => s === GLOBSTAR)) { + if (segments.every(segment => segment === GLOBSTAR)) { regEx = '.*'; } @@ -127,116 +128,127 @@ function parseRegExp(pattern: string): string { let previousSegmentWasGlobStar = false; segments.forEach((segment, index) => { - // Globstar is special + // Treat globstar specially if (segment === GLOBSTAR) { // if we have more than one globstar after another, just ignore it - if (!previousSegmentWasGlobStar) { - regEx += starsToRegExp(2); - previousSegmentWasGlobStar = true; + if (previousSegmentWasGlobStar) { + return; } - return; + regEx += starsToRegExp(2, index === segments.length - 1); } - // States - let inBraces = false; - let braceVal = ''; + // Anything else, not globstar + else { - let inBrackets = false; - let bracketVal = ''; + // States + let inBraces = false; + let braceVal = ''; - for (const char of segment) { - // Support brace expansion - if (char !== '}' && inBraces) { - braceVal += char; - continue; + let inBrackets = false; + let bracketVal = ''; + + for (const char of segment) { + + // Support brace expansion + if (char !== '}' && inBraces) { + braceVal += char; + continue; + } + + // Support brackets + if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) { + let res: string; + + // range operator + if (char === '-') { + res = char; + } + + // negation operator (only valid on first index in bracket) + else if ((char === '^' || char === '!') && !bracketVal) { + res = '^'; + } + + // glob split matching is not allowed within character ranges + // see http://man7.org/linux/man-pages/man7/glob.7.html + else if (char === GLOB_SPLIT) { + res = ''; + } + + // anything else gets escaped + else { + res = escapeRegExpCharacters(char); + } + + bracketVal += res; + continue; + } + + switch (char) { + case '{': + inBraces = true; + continue; + + case '[': + inBrackets = true; + continue; + + case '}': { + const choices = splitGlobAware(braceVal, ','); + + // Converts {foo,bar} => [foo|bar] + const braceRegExp = `(?:${choices.map(choice => parseRegExp(choice)).join('|')})`; + + regEx += braceRegExp; + + inBraces = false; + braceVal = ''; + + break; + } + + case ']': { + regEx += ('[' + bracketVal + ']'); + + inBrackets = false; + bracketVal = ''; + + break; + } + + case '?': + regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \) + continue; + + case '*': + regEx += starsToRegExp(1); + continue; + + default: + regEx += escapeRegExpCharacters(char); + } } - // Support brackets - if (inBrackets && (char !== ']' || !bracketVal) /* ] is literally only allowed as first character in brackets to match it */) { - let res: string; - - // range operator - if (char === '-') { - res = char; - } - - // negation operator (only valid on first index in bracket) - else if ((char === '^' || char === '!') && !bracketVal) { - res = '^'; - } - - // glob split matching is not allowed within character ranges - // see http://man7.org/linux/man-pages/man7/glob.7.html - else if (char === GLOB_SPLIT) { - res = ''; - } - - // anything else gets escaped - else { - res = escapeRegExpCharacters(char); - } - - bracketVal += res; - continue; - } - - switch (char) { - case '{': - inBraces = true; - continue; - - case '[': - inBrackets = true; - continue; - - case '}': { - const choices = splitGlobAware(braceVal, ','); - - // Converts {foo,bar} => [foo|bar] - const braceRegExp = `(?:${choices.map(c => parseRegExp(c)).join('|')})`; - - regEx += braceRegExp; - - inBraces = false; - braceVal = ''; - - break; - } - case ']': - regEx += ('[' + bracketVal + ']'); - - inBrackets = false; - bracketVal = ''; - - break; - - - case '?': - regEx += NO_PATH_REGEX; // 1 ? matches any single character except path separator (/ and \) - continue; - - case '*': - regEx += starsToRegExp(1); - continue; - - default: - regEx += escapeRegExpCharacters(char); + // Tail: Add the slash we had split on if there is more to + // come and the remaining pattern is not a globstar + // For example if pattern: some/**/*.js we want the "/" after + // some to be included in the RegEx to prevent a folder called + // "something" to match as well. + if ( + index < segments.length - 1 && // more segments to come after this + ( + segments[index + 1] !== GLOBSTAR || // next segment is not **, or... + index + 2 < segments.length // ...next segment is ** but there is more segments after that + ) + ) { + regEx += PATH_REGEX; } } - // Tail: Add the slash we had split on if there is more to come and the remaining pattern is not a globstar - // For example if pattern: some/**/*.js we want the "/" after some to be included in the RegEx to prevent - // a folder called "something" to match as well. - // However, if pattern: some/**, we tolerate that we also match on "something" because our globstar behaviour - // is to match 0-N segments. - if (index < segments.length - 1 && (segments[index + 1] !== GLOBSTAR || index + 2 < segments.length)) { - regEx += PATH_REGEX; - } - - // reset state - previousSegmentWasGlobStar = false; + // update globstar state + previousSegmentWasGlobStar = (segment === GLOBSTAR); }); } @@ -244,12 +256,12 @@ function parseRegExp(pattern: string): string { } // regexes to check for trivial glob patterns that just check for String#endsWith -const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something -const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something +const T1 = /^\*\*\/\*\.[\w\.-]+$/; // **/*.something +const T2 = /^\*\*\/([\w\.-]+)\/?$/; // **/something const T3 = /^{\*\*\/\*?[\w\.-]+\/?(,\*\*\/\*?[\w\.-]+\/?)*}$/; // {**/*.something,**/*.else} or {**/package.json,**/project.json} const T3_2 = /^{\*\*\/\*?[\w\.-]+(\/(\*\*)?)?(,\*\*\/\*?[\w\.-]+(\/(\*\*)?)?)*}$/; // Like T3, with optional trailing /** -const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else -const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else +const T4 = /^\*\*((\/[\w\.-]+)+)\/?$/; // **/something/else +const T5 = /^([\w\.-]+(\/[\w\.-]+)*)\/?$/; // something/else export type ParsedPattern = (path: string, basename?: string) => boolean; @@ -257,7 +269,7 @@ export type ParsedPattern = (path: string, basename?: string) => boolean; // iff `hasSibling` returns a `Promise`. export type ParsedExpression = (path: string, basename?: string, hasSibling?: (name: string) => boolean | Promise) => string | null | Promise /* the matching pattern */; -export interface IGlobOptions { +interface IGlobOptions { /** * Simplify patterns for use as exclusion filters during @@ -274,6 +286,7 @@ interface ParsedStringPattern { allBasenames?: string[]; allPaths?: string[]; } + interface ParsedExpressionPattern { (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise): string | null | Promise /* the matching pattern */; requiresSiblings?: boolean; @@ -296,7 +309,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P return NULL; } - // Handle IRelativePattern + // Handle relative patterns let pattern: string; if (typeof arg1 !== 'string') { pattern = arg1.pattern; @@ -316,18 +329,15 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P // Check for Trivials let match: RegExpExecArray | null; - if (T1.test(pattern)) { // common pattern: **/*.txt just need endsWith check - const base = pattern.substr(4); // '**/*'.length === 4 - parsedPattern = function (path, basename) { - return typeof path === 'string' && path.endsWith(base) ? pattern : null; - }; - } else if (match = T2.exec(trimForExclusions(pattern, options))) { // common pattern: **/some.txt just need basename check + if (T1.test(pattern)) { + parsedPattern = trivia1(pattern.substr(4), pattern); // common pattern: **/*.txt just need endsWith check + } else if (match = T2.exec(trimForExclusions(pattern, options))) { // common pattern: **/some.txt just need basename check parsedPattern = trivia2(match[1], pattern); } else if ((options.trimForExclusions ? T3_2 : T3).test(pattern)) { // repetition of common patterns (see above) {**/*.txt,**/*.png} parsedPattern = trivia3(pattern, options); - } else if (match = T4.exec(trimForExclusions(pattern, options))) { // common pattern: **/something/else just need endsWith check + } else if (match = T4.exec(trimForExclusions(pattern, options))) { // common pattern: **/something/else just need endsWith check parsedPattern = trivia4and5(match[1].substr(1), pattern, true); - } else if (match = T5.exec(trimForExclusions(pattern, options))) { // common pattern: something/else just need equals check + } else if (match = T5.exec(trimForExclusions(pattern, options))) { // common pattern: something/else just need equals check parsedPattern = trivia4and5(match[1], pattern, false); } @@ -364,54 +374,74 @@ function trimForExclusions(pattern: string, options: IGlobOptions): string { return options.trimForExclusions && pattern.endsWith('/**') ? pattern.substr(0, pattern.length - 2) : pattern; // dropping **, tailing / is dropped later } +// common pattern: **/*.txt just need endsWith check +function trivia1(base: string, pattern: string): ParsedStringPattern { + return function (path: string, basename?: string) { + return typeof path === 'string' && path.endsWith(base) ? pattern : null; + }; +} + // common pattern: **/some.txt just need basename check -function trivia2(base: string, originalPattern: string): ParsedStringPattern { +function trivia2(base: string, pattern: string): ParsedStringPattern { const slashBase = `/${base}`; const backslashBase = `\\${base}`; - const parsedPattern: ParsedStringPattern = function (path, basename) { + + const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) { if (typeof path !== 'string') { return null; } + if (basename) { - return basename === base ? originalPattern : null; + return basename === base ? pattern : null; } - return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? originalPattern : null; + + return path === base || path.endsWith(slashBase) || path.endsWith(backslashBase) ? pattern : null; }; + const basenames = [base]; parsedPattern.basenames = basenames; - parsedPattern.patterns = [originalPattern]; + parsedPattern.patterns = [pattern]; parsedPattern.allBasenames = basenames; + return parsedPattern; } // repetition of common patterns (see above) {**/*.txt,**/*.png} function trivia3(pattern: string, options: IGlobOptions): ParsedStringPattern { - const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1).split(',') + const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1) + .split(',') .map(pattern => parsePattern(pattern, options)) .filter(pattern => pattern !== NULL), pattern); - const n = parsedPatterns.length; - if (!n) { + + const patternsLength = parsedPatterns.length; + if (!patternsLength) { return NULL; } - if (n === 1) { - return parsedPatterns[0]; + + if (patternsLength === 1) { + return parsedPatterns[0]; } + const parsedPattern: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { - if ((parsedPatterns[i])(path, basename)) { + if (parsedPatterns[i](path, basename)) { return pattern; } } + return null; }; - const withBasenames = parsedPatterns.find(pattern => !!(pattern).allBasenames); + + const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames); if (withBasenames) { - parsedPattern.allBasenames = (withBasenames).allBasenames; + parsedPattern.allBasenames = withBasenames.allBasenames; } - const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]); if (allPaths.length) { parsedPattern.allPaths = allPaths; } + return parsedPattern; } @@ -422,18 +452,19 @@ function trivia4and5(targetPath: string, pattern: string, matchPathEnds: boolean const nativePathEnd = sep + nativePath; const targetPathEnd = posix.sep + targetPath; - const parsedPattern: ParsedStringPattern = matchPathEnds ? function (testPath, basename) { - return typeof testPath === 'string' && - ((testPath === nativePath || testPath.endsWith(nativePathEnd)) - || !usingPosixSep && (testPath === targetPath || testPath.endsWith(targetPathEnd))) - ? pattern : null; - } : function (testPath, basename) { - return typeof testPath === 'string' && - (testPath === nativePath - || (!usingPosixSep && testPath === targetPath)) - ? pattern : null; - }; + let parsedPattern: ParsedStringPattern; + if (matchPathEnds) { + parsedPattern = function (path: string, basename?: string) { + return typeof path === 'string' && ((path === nativePath || path.endsWith(nativePathEnd)) || !usingPosixSep && (path === targetPath || path.endsWith(targetPathEnd))) ? pattern : null; + }; + } else { + parsedPattern = function (path: string, basename?: string) { + return typeof path === 'string' && (path === nativePath || (!usingPosixSep && path === targetPath)) ? pattern : null; + }; + } + parsedPattern.allPaths = [(matchPathEnds ? '*/' : './') + targetPath]; + return parsedPattern; } @@ -442,6 +473,7 @@ function toRegExp(pattern: string): ParsedStringPattern { const regExp = new RegExp(`^${parseRegExp(pattern)}$`); return function (path: string) { regExp.lastIndex = 0; // reset RegExp to its initial state to reuse it! + return typeof path === 'string' && regExp.test(path) ? pattern : null; }; } catch (error) { @@ -465,7 +497,7 @@ export function match(arg1: string | IExpression | IRelativePattern, path: strin return false; } - return parse(arg1)(path, undefined, hasSibling); + return parse(arg1)(path, undefined, hasSibling); } /** @@ -479,6 +511,7 @@ export function match(arg1: string | IExpression | IRelativePattern, path: strin */ export function parse(pattern: string | IRelativePattern, options?: IGlobOptions): ParsedPattern; export function parse(expression: IExpression, options?: IGlobOptions): ParsedExpression; +export function parse(arg1: string | IExpression | IRelativePattern, options?: IGlobOptions): ParsedPattern | ParsedExpression; export function parse(arg1: string | IExpression | IRelativePattern, options: IGlobOptions = {}): ParsedPattern | ParsedExpression { if (!arg1) { return FALSE; @@ -490,15 +523,19 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG if (parsedPattern === NULL) { return FALSE; } + const resultPattern: ParsedPattern & { allBasenames?: string[]; allPaths?: string[] } = function (path: string, basename?: string) { return !!parsedPattern(path, basename); }; + if (parsedPattern.allBasenames) { resultPattern.allBasenames = parsedPattern.allBasenames; } + if (parsedPattern.allPaths) { resultPattern.allPaths = parsedPattern.allPaths; } + return resultPattern; } @@ -506,44 +543,6 @@ export function parse(arg1: string | IExpression | IRelativePattern, options: IG return parsedExpression(arg1, options); } -export function hasSiblingPromiseFn(siblingsFn?: () => Promise) { - if (!siblingsFn) { - return undefined; - } - - let siblings: Promise>; - return (name: string) => { - if (!siblings) { - siblings = (siblingsFn() || Promise.resolve([])) - .then(list => list ? listToMap(list) : {}); - } - return siblings.then(map => !!map[name]); - }; -} - -export function hasSiblingFn(siblingsFn?: () => string[]) { - if (!siblingsFn) { - return undefined; - } - - let siblings: Record; - return (name: string) => { - if (!siblings) { - const list = siblingsFn(); - siblings = list ? listToMap(list) : {}; - } - return !!siblings[name]; - }; -} - -function listToMap(list: string[]) { - const map: Record = {}; - for (const key of list) { - map[key] = true; - } - return map; -} - export function isRelativePattern(obj: unknown): obj is IRelativePattern { const rp = obj as IRelativePattern | undefined | null; if (!rp) { @@ -566,20 +565,21 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse .map(pattern => parseExpressionPattern(pattern, expression[pattern], options)) .filter(pattern => pattern !== NULL)); - const n = parsedPatterns.length; - if (!n) { + const patternsLength = parsedPatterns.length; + if (!patternsLength) { return NULL; } if (!parsedPatterns.some(parsedPattern => !!(parsedPattern).requiresSiblings)) { - if (n === 1) { - return parsedPatterns[0]; + if (patternsLength === 1) { + return parsedPatterns[0] as ParsedStringPattern; } const resultExpression: ParsedStringPattern = function (path: string, basename?: string) { for (let i = 0, n = parsedPatterns.length; i < n; i++) { - // Pattern matches path - const result = (parsedPatterns[i])(path, basename); + + // Check if pattern matches path + const result = parsedPatterns[i](path, basename); if (result) { return result; } @@ -588,12 +588,12 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return null; }; - const withBasenames = parsedPatterns.find(pattern => !!(pattern).allBasenames); + const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames); if (withBasenames) { - resultExpression.allBasenames = (withBasenames).allBasenames; + resultExpression.allBasenames = withBasenames.allBasenames; } - const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]); if (allPaths.length) { resultExpression.allPaths = allPaths; } @@ -605,16 +605,19 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse let name: string | undefined = undefined; for (let i = 0, n = parsedPatterns.length; i < n; i++) { + // Pattern matches path const parsedPattern = (parsedPatterns[i]); if (parsedPattern.requiresSiblings && hasSibling) { if (!base) { base = basename(path); } + if (!name) { name = base.substr(0, base.length - extname(path).length); } } + const result = parsedPattern(path, base, name, hasSibling); if (result) { return result; @@ -624,12 +627,12 @@ function parsedExpression(expression: IExpression, options: IGlobOptions): Parse return null; }; - const withBasenames = parsedPatterns.find(pattern => !!(pattern).allBasenames); + const withBasenames = parsedPatterns.find(pattern => !!pattern.allBasenames); if (withBasenames) { - resultExpression.allBasenames = (withBasenames).allBasenames; + resultExpression.allBasenames = withBasenames.allBasenames; } - const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, []); + const allPaths = parsedPatterns.reduce((all, current) => current.allPaths ? all.concat(current.allPaths) : all, [] as string[]); if (allPaths.length) { resultExpression.allPaths = allPaths; } @@ -654,7 +657,7 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, // Expression Pattern is if (value) { - const when = (value).when; + const when = value.when; if (typeof when === 'string') { const result: ParsedExpressionPattern = (path: string, basename?: string, name?: string, hasSibling?: (name: string) => boolean | Promise) => { if (!hasSibling || !parsedPattern(path, basename)) { @@ -664,15 +667,17 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, const clausePattern = when.replace('$(basename)', name!); const matched = hasSibling(clausePattern); return isThenable(matched) ? - matched.then(m => m ? pattern : null) : + matched.then(match => match ? pattern : null) : matched ? pattern : null; }; + result.requiresSiblings = true; + return result; } } - // Expression is Anything + // Expression is anything return parsedPattern; } @@ -684,24 +689,30 @@ function aggregateBasenameMatches(parsedPatterns: Array((all, current) => { const basenames = (current).basenames; + return basenames ? all.concat(basenames) : all; - }, []); + }, [] as string[]); + let patterns: string[]; if (result) { patterns = []; + for (let i = 0, n = basenames.length; i < n; i++) { patterns.push(result); } } else { patterns = basenamePatterns.reduce((all, current) => { const patterns = (current).patterns; + return patterns ? all.concat(patterns) : all; - }, []); + }, [] as string[]); } - const aggregate: ParsedStringPattern = function (path, basename) { + + const aggregate: ParsedStringPattern = function (path: string, basename?: string) { if (typeof path !== 'string') { return null; } + if (!basename) { let i: number; for (i = path.length; i > 0; i--) { @@ -710,11 +721,14 @@ function aggregateBasenameMatches(parsedPatterns: Array { + if (value.charAt(offset - 1) !== '\\') { + return `\\${match}`; + } else { + return match; + } + }); + } } export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[] | null | undefined): boolean { diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 8f11810ba62..d74f249a405 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -52,7 +52,7 @@ export namespace Schemas { export const vscodeRemoteResource = 'vscode-remote-resource'; - export const userData = 'vscode-userdata'; + export const vscodeUserData = 'vscode-userdata'; export const vscodeCustomEditor = 'vscode-custom-editor'; diff --git a/src/vs/base/common/objects.ts b/src/vs/base/common/objects.ts index 148d2d6b232..965f997fbd7 100644 --- a/src/vs/base/common/objects.ts +++ b/src/vs/base/common/objects.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { isArray, isObject, isUndefinedOrNull } from 'vs/base/common/types'; +import { isArray, isTypedArray, isObject, isUndefinedOrNull } from 'vs/base/common/types'; export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { @@ -35,7 +35,7 @@ export function deepFreeze(obj: T): T { for (const key in obj) { if (_hasOwnProperty.call(obj, key)) { const prop = obj[key]; - if (typeof prop === 'object' && !Object.isFrozen(prop)) { + if (typeof prop === 'object' && !Object.isFrozen(prop) && !isTypedArray(prop)) { stack.push(prop); } } @@ -46,6 +46,7 @@ export function deepFreeze(obj: T): T { const _hasOwnProperty = Object.prototype.hasOwnProperty; + export function cloneAndChange(obj: any, changer: (orig: any) => any): any { return _cloneAndChange(obj, changer, new Set()); } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 1edc76fbe77..fd05d5a160c 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -91,6 +91,8 @@ export interface IProductConfiguration { readonly productName: string; }; + readonly removeTelemetryMachineId?: boolean; + readonly enabledTelemetryLevels?: { error: boolean; usage: boolean }; readonly enableTelemetry?: boolean; readonly openToWelcomeMainPage?: boolean; readonly aiConfig?: { @@ -138,8 +140,7 @@ export interface IProductConfiguration { readonly extensionKind?: { readonly [extensionId: string]: ('ui' | 'workspace' | 'web')[] }; readonly extensionPointExtensionKind?: { readonly [extensionPointId: string]: ('ui' | 'workspace' | 'web')[] }; readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[] }; - /** @deprecated */ - readonly extensionAllowedProposedApi?: readonly string[]; + readonly extensionEnabledApiProposals?: { readonly [extensionId: string]: string[] }; readonly extensionUntrustedWorkspaceSupport?: { readonly [extensionId: string]: ExtensionUntrustedWorkspaceSupport }; readonly extensionVirtualWorkspacesSupport?: { readonly [extensionId: string]: ExtensionVirtualWorkspaceSupport }; diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 602330b8bbc..c2245c67558 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -42,6 +42,25 @@ export function isObject(obj: unknown): obj is Object { && !(obj instanceof Date); } +/** + * + * @returns whether the provided parameter is of type `Buffer` or Uint8Array dervived type + */ +export function isTypedArray(obj: unknown): obj is Object { + return typeof obj === 'object' + && (obj instanceof Uint8Array || + obj instanceof Uint16Array || + obj instanceof Uint32Array || + obj instanceof Float32Array || + obj instanceof Float64Array || + obj instanceof Int8Array || + obj instanceof Int16Array || + obj instanceof Int32Array || + obj instanceof BigInt64Array || + obj instanceof BigUint64Array || + obj instanceof Uint8ClampedArray); +} + /** * In **contrast** to just checking `typeof` this will return `false` for `NaN`. * @returns whether the provided parameter is a JavaScript Number or not. diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js index 5c14fade878..f99d3808de5 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -46,7 +46,7 @@ * @returns {Promise} */ function rimraf(location) { - return new Promise((c, e) => fs.rmdir(location, { recursive: true }, err => (err && err.code !== 'ENOENT') ? e(err) : c())); + return new Promise((c, e) => fs.rm(location, { recursive: true, force: true, maxRetries: 3 }, err => err ? e(err) : c())); } /** diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index c04d2722df6..2283c4a61d6 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -43,7 +43,7 @@ async function rimraf(path: string, mode = RimRafMode.UNLINK): Promise { throw new Error('rimraf - will refuse to recursively delete root'); } - // delete: via rmDir + // delete: via rm if (mode === RimRafMode.UNLINK) { return rimrafUnlink(path); } @@ -79,7 +79,7 @@ async function rimrafMove(path: string): Promise { } async function rimrafUnlink(path: string): Promise { - return Promises.rmdir(path, { recursive: true, maxRetries: 3 }); + return promisify(fs.rm)(path, { recursive: true, force: true, maxRetries: 3 }); } export function rimrafSync(path: string): void { @@ -87,7 +87,7 @@ export function rimrafSync(path: string): void { throw new Error('rimraf - will refuse to recursively delete root'); } - fs.rmdirSync(path, { recursive: true }); + fs.rmSync(path, { recursive: true, force: true, maxRetries: 3 }); } //#endregion diff --git a/src/vs/base/parts/quickinput/browser/quickInputUtils.ts b/src/vs/base/parts/quickinput/browser/quickInputUtils.ts index 11f24b92683..f6d5e859ea2 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputUtils.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputUtils.ts @@ -22,7 +22,7 @@ export function getIconClass(iconPath: { dark: URI; light?: URI } | undefined): iconClass = iconPathToClass[key]; } else { iconClass = iconClassGenerator.nextId(); - dom.createCSSRule(`.${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`); + dom.createCSSRule(`.${iconClass}, .hc-light .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.light || iconPath.dark)}`); dom.createCSSRule(`.vs-dark .${iconClass}, .hc-black .${iconClass}`, `background-image: ${dom.asCSSUrl(iconPath.dark)}`); iconPathToClass[key] = iconClass; } diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts index 01622e6bbab..d2d75da87ce 100644 --- a/src/vs/base/parts/sandbox/common/electronTypes.ts +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -7,11 +7,10 @@ // ####################################################################### // ### ### // ### electron.d.ts types we need in a common layer for reuse ### -// ### (copied from Electron 11.x) ### +// ### (copied from Electron 16.x) ### // ### ### // ####################################################################### - export interface MessageBoxOptions { /** * Content of the message box. @@ -34,6 +33,13 @@ export interface MessageBoxOptions { * the message box opens. */ defaultId?: number; + /** + * Pass an instance of AbortSignal to optionally close the message box, the message + * box will behave as if it was cancelled by the user. On macOS, `signal` does not + * work with message boxes that do not have a parent window, since those message + * boxes run synchronously due to platform limitations. + */ + signal?: AbortSignal; /** * Title of the message box, some platforms will not show it. */ @@ -50,7 +56,12 @@ export interface MessageBoxOptions { * Initial checked state of the checkbox. `false` by default. */ checkboxChecked?: boolean; - // icon?: NativeImage; + /** + * Custom width of the text in the message box. + * + * @platform darwin + */ + textWidth?: number; /** * The index of the button to be used to cancel the dialog, via the `Esc` key. By * default this is assigned to the first button with "cancel" or "no" as the label. @@ -88,21 +99,10 @@ export interface MessageBoxReturnValue { checkboxChecked: boolean; } -export interface OpenDevToolsOptions { - /** - * Opens the devtools with specified dock state, can be `right`, `bottom`, - * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's - * possible to dock back. In `detach` mode it's not. - */ - mode: ('right' | 'bottom' | 'undocked' | 'detach'); - /** - * Whether to bring the opened devtools window to the foreground. The default is - * `true`. - */ - activate?: boolean; -} - export interface SaveDialogOptions { + /** + * The dialog title. Cannot be displayed on some _Linux_ desktop environments. + */ title?: string; /** * Absolute directory path, absolute file path, or file name to use by default. @@ -143,6 +143,25 @@ export interface SaveDialogOptions { securityScopedBookmarks?: boolean; } +export interface SaveDialogReturnValue { + /** + * whether or not the dialog was canceled. + */ + canceled: boolean; + /** + * If the dialog is canceled, this will be `undefined`. + */ + filePath?: string; + /** + * Base64 encoded string which contains the security scoped bookmark data for the + * saved file. `securityScopedBookmarks` must be enabled for this to be present. + * (For return values, see table here.) + * + * @platform darwin,mas + */ + bookmark?: string; +} + export interface OpenDialogOptions { title?: string; defaultPath?: string; @@ -191,25 +210,6 @@ export interface OpenDialogReturnValue { bookmarks?: string[]; } -export interface SaveDialogReturnValue { - /** - * whether or not the dialog was canceled. - */ - canceled: boolean; - /** - * If the dialog is canceled, this will be `undefined`. - */ - filePath?: string; - /** - * Base64 encoded string which contains the security scoped bookmark data for the - * saved file. `securityScopedBookmarks` must be enabled for this to be present. - * (For return values, see table here.) - * - * @platform darwin,mas - */ - bookmark?: string; -} - export interface FileFilter { // Docs: https://electronjs.org/docs/api/structures/file-filter @@ -218,6 +218,20 @@ export interface FileFilter { name: string; } +export interface OpenDevToolsOptions { + /** + * Opens the devtools with specified dock state, can be `right`, `bottom`, + * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's + * possible to dock back. In `detach` mode it's not. + */ + mode: ('right' | 'bottom' | 'undocked' | 'detach'); + /** + * Whether to bring the opened devtools window to the foreground. The default is + * `true`. + */ + activate?: boolean; +} + export interface InputEvent { // Docs: https://electronjs.org/docs/api/structures/input-event diff --git a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts index 8f36cb6f045..58589dbec1d 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/electronTypes.ts @@ -7,7 +7,7 @@ // ####################################################################### // ### ### // ### electron.d.ts types we expose from electron-sandbox ### -// ### (copied from Electron 11.x) ### +// ### (copied from Electron 16.x) ### // ### ### // ####################################################################### diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 12b775a3e6c..b7f56b564b4 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -562,8 +562,8 @@ suite('Async', () => { test('events', async function () { let queue = new async.Queue(); - let finished = false; - const onFinished = Event.toPromise(queue.onFinished).then(() => finished = true); + let drained = false; + const onDrained = Event.toPromise(queue.onDrained).then(() => drained = true); let res: number[] = []; @@ -576,14 +576,14 @@ suite('Async', () => { queue.queue(f3); q1.then(() => { - assert.ok(!finished); + assert.ok(!drained); q2.then(() => { - assert.ok(!finished); + assert.ok(!drained); }); }); - await onFinished; - assert.ok(finished); + await onDrained; + assert.ok(drained); }); }); diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index 6133da6b5eb..ba31ed0099c 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -123,11 +123,14 @@ suite('Glob', () => { p = '**/.*'; assertGlobMatch(p, '.git'); + assertGlobMatch(p, '/.git'); assertGlobMatch(p, '.hidden.txt'); assertNoGlobMatch(p, 'git'); assertNoGlobMatch(p, 'hidden.txt'); assertGlobMatch(p, 'path/.git'); assertGlobMatch(p, 'path/.hidden.txt'); + assertGlobMatch(p, '/path/.git'); + assertGlobMatch(p, '/path/.hidden.txt'); assertNoGlobMatch(p, 'path/git'); assertNoGlobMatch(p, 'pat.h/hidden.txt'); @@ -147,6 +150,8 @@ suite('Glob', () => { assertNoGlobMatch(p, 'hidden._txt'); assertGlobMatch(p, 'path/._git'); assertGlobMatch(p, 'path/._hidden.txt'); + assertGlobMatch(p, '/path/._git'); + assertGlobMatch(p, '/path/._hidden.txt'); assertNoGlobMatch(p, 'path/git'); assertNoGlobMatch(p, 'pat.h/hidden._txt'); }); @@ -206,6 +211,13 @@ suite('Glob', () => { assertGlobMatch(p, 'a/node_modules/'); assertGlobMatch(p, 'node_modules/foo'); assertGlobMatch(p, 'foo/node_modules/foo/bar'); + + assertGlobMatch(p, '/node_modules'); + assertGlobMatch(p, '/node_modules/'); + assertGlobMatch(p, '/a/node_modules'); + assertGlobMatch(p, '/a/node_modules/'); + assertGlobMatch(p, '/node_modules/foo'); + assertGlobMatch(p, '/foo/node_modules/foo/bar'); }); test('questionmark', () => { @@ -229,6 +241,7 @@ suite('Glob', () => { let p = '**/*.js'; assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, '/foo.js'); assertGlobMatch(p, 'folder/foo.js'); assertGlobMatch(p, '/node_modules/foo.js'); assertNoGlobMatch(p, 'foo.jss'); @@ -241,6 +254,7 @@ suite('Glob', () => { assertGlobMatch(p, 'project.json'); assertGlobMatch(p, '/project.json'); assertGlobMatch(p, 'some/folder/project.json'); + assertGlobMatch(p, '/some/folder/project.json'); assertNoGlobMatch(p, 'some/folder/file_project.json'); assertNoGlobMatch(p, 'some/folder/fileproject.json'); assertNoGlobMatch(p, 'some/rrproject.json'); @@ -248,13 +262,17 @@ suite('Glob', () => { p = 'test/**'; assertGlobMatch(p, 'test'); + assertGlobMatch(p, 'test/foo'); + assertGlobMatch(p, 'test/foo/'); assertGlobMatch(p, 'test/foo.js'); assertGlobMatch(p, 'test/other/foo.js'); assertNoGlobMatch(p, 'est/other/foo.js'); p = '**'; + assertGlobMatch(p, '/'); assertGlobMatch(p, 'foo.js'); assertGlobMatch(p, 'folder/foo.js'); + assertGlobMatch(p, 'folder/foo/'); assertGlobMatch(p, '/node_modules/foo.js'); assertGlobMatch(p, 'foo.jss'); assertGlobMatch(p, 'some.js/test'); @@ -270,6 +288,7 @@ suite('Glob', () => { p = '**/**/*.js'; assertGlobMatch(p, 'foo.js'); + assertGlobMatch(p, '/foo.js'); assertGlobMatch(p, 'folder/foo.js'); assertGlobMatch(p, '/node_modules/foo.js'); assertNoGlobMatch(p, 'foo.jss'); @@ -280,7 +299,9 @@ suite('Glob', () => { assertNoGlobMatch(p, 'foo.js'); assertNoGlobMatch(p, 'folder/foo.js'); assertGlobMatch(p, 'node_modules/foo.js'); + assertGlobMatch(p, '/node_modules/foo.js'); assertGlobMatch(p, 'node_modules/some/folder/foo.js'); + assertGlobMatch(p, '/node_modules/some/folder/foo.js'); assertNoGlobMatch(p, 'node_modules/some/folder/foo.ts'); assertNoGlobMatch(p, 'foo.jss'); assertNoGlobMatch(p, 'some.js/test'); @@ -292,6 +313,8 @@ suite('Glob', () => { assertGlobMatch(p, '/node_modules/more'); assertGlobMatch(p, 'some/test/node_modules'); assertGlobMatch(p, 'some\\test\\node_modules'); + assertGlobMatch(p, '/some/test/node_modules'); + assertGlobMatch(p, '\\some\\test\\node_modules'); assertGlobMatch(p, 'C:\\\\some\\test\\node_modules'); assertGlobMatch(p, 'C:\\\\some\\test\\node_modules\\more'); @@ -300,6 +323,8 @@ suite('Glob', () => { assertGlobMatch(p, '/bower_components'); assertGlobMatch(p, 'some/test/bower_components'); assertGlobMatch(p, 'some\\test\\bower_components'); + assertGlobMatch(p, '/some/test/bower_components'); + assertGlobMatch(p, '\\some\\test\\bower_components'); assertGlobMatch(p, 'C:\\\\some\\test\\bower_components'); assertGlobMatch(p, 'C:\\\\some\\test\\bower_components\\more'); @@ -307,12 +332,16 @@ suite('Glob', () => { assertGlobMatch(p, '/.git'); assertGlobMatch(p, 'some/test/.git'); assertGlobMatch(p, 'some\\test\\.git'); + assertGlobMatch(p, '/some/test/.git'); + assertGlobMatch(p, '\\some\\test\\.git'); assertGlobMatch(p, 'C:\\\\some\\test\\.git'); assertNoGlobMatch(p, 'tempting'); assertNoGlobMatch(p, '/tempting'); assertNoGlobMatch(p, 'some/test/tempting'); assertNoGlobMatch(p, 'some\\test\\tempting'); + assertNoGlobMatch(p, '/some/test/tempting'); + assertNoGlobMatch(p, '\\some\\test\\tempting'); assertNoGlobMatch(p, 'C:\\\\some\\test\\tempting'); p = '{**/package.json,**/project.json}'; @@ -370,14 +399,23 @@ suite('Glob', () => { assertGlobMatch(p, 'test/bar'); assertGlobMatch(p, 'other/more/foo'); assertGlobMatch(p, 'other/more/bar'); + assertGlobMatch(p, '/foo'); + assertGlobMatch(p, '/bar'); + assertGlobMatch(p, '/test/foo'); + assertGlobMatch(p, '/test/bar'); + assertGlobMatch(p, '/other/more/foo'); + assertGlobMatch(p, '/other/more/bar'); p = '{foo,bar}/**'; assertGlobMatch(p, 'foo'); assertGlobMatch(p, 'bar'); + assertGlobMatch(p, 'bar/'); assertGlobMatch(p, 'foo/test'); assertGlobMatch(p, 'bar/test'); + assertGlobMatch(p, 'bar/test/'); assertGlobMatch(p, 'foo/other/more'); assertGlobMatch(p, 'bar/other/more'); + assertGlobMatch(p, 'bar/other/more/'); p = '{**/*.d.ts,**/*.js}'; @@ -543,12 +581,10 @@ suite('Glob', () => { test('full path', function () { assertGlobMatch('testing/this/foo.txt', 'testing/this/foo.txt'); - // assertGlobMatch('testing/this/foo.txt', 'testing\\this\\foo.txt'); }); test('ending path', function () { assertGlobMatch('**/testing/this/foo.txt', 'some/path/testing/this/foo.txt'); - // assertGlobMatch('**/testing/this/foo.txt', 'some\\path\\testing\\this\\foo.txt'); }); test('prefix agnostic', function () { @@ -689,6 +725,31 @@ suite('Glob', () => { assert.strictEqual(glob.match(expr, 'foo.as'), null); }); + test('expression with non-trivia glob (issue 144458)', function () { + let pattern = '**/p*'; + + assert.strictEqual(glob.match(pattern, 'foo/barp'), false); + assert.strictEqual(glob.match(pattern, 'foo/bar/ap'), false); + assert.strictEqual(glob.match(pattern, 'ap'), false); + + assert.strictEqual(glob.match(pattern, 'foo/barp1'), false); + assert.strictEqual(glob.match(pattern, 'foo/bar/ap1'), false); + assert.strictEqual(glob.match(pattern, 'ap1'), false); + + assert.strictEqual(glob.match(pattern, '/foo/barp'), false); + assert.strictEqual(glob.match(pattern, '/foo/bar/ap'), false); + assert.strictEqual(glob.match(pattern, '/ap'), false); + + assert.strictEqual(glob.match(pattern, '/foo/barp1'), false); + assert.strictEqual(glob.match(pattern, '/foo/bar/ap1'), false); + assert.strictEqual(glob.match(pattern, '/ap1'), false); + + assert.strictEqual(glob.match(pattern, 'foo/pbar'), true); + assert.strictEqual(glob.match(pattern, '/foo/pbar'), true); + assert.strictEqual(glob.match(pattern, 'foo/bar/pa'), true); + assert.strictEqual(glob.match(pattern, '/p'), true); + }); + test('expression with empty glob', function () { let expr = { '': true }; diff --git a/src/vs/base/test/common/markdownString.test.ts b/src/vs/base/test/common/markdownString.test.ts index 7d9d64f2b69..b008091a46f 100644 --- a/src/vs/base/test/common/markdownString.test.ts +++ b/src/vs/base/test/common/markdownString.test.ts @@ -28,6 +28,40 @@ suite('MarkdownString', () => { assert.strictEqual(mds.value, '\\# foo\n\n\\*bar\\*'); }); + test('appendLink', function () { + + function assertLink(target: string, label: string, title: string | undefined, expected: string) { + const mds = new MarkdownString(); + mds.appendLink(target, label, title); + assert.strictEqual(mds.value, expected); + } + + assertLink( + 'https://example.com\\()![](file:///Users/jrieken/Code/_samples/devfest/foo/img.png)', 'hello', undefined, + '[hello](https://example.com\\(\\)![](file:///Users/jrieken/Code/_samples/devfest/foo/img.png\\))' + ); + assertLink( + 'https://example.com', 'hello', 'title', + '[hello](https://example.com "title")' + ); + assertLink( + 'foo)', 'hello]', undefined, + '[hello\\]](foo\\))' + ); + assertLink( + 'foo\\)', 'hello]', undefined, + '[hello\\]](foo\\))' + ); + assertLink( + 'fo)o', 'hell]o', undefined, + '[hell\\]o](fo\\)o)' + ); + assertLink( + 'foo)', 'hello]', 'title"', + '[hello\\]](foo\\) "title\\"")' + ); + }); + suite('ThemeIcons', () => { suite('Support On', () => { diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 09bc57bf432..e45782e236f 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -273,7 +273,7 @@ flakySuite('PFS', function () { const linkTarget = await Promises.readlink(targetLinkMD5JSFolderLinked); assert.strictEqual(linkTarget, targetLinkMD5JSFolder); - await Promises.rmdir(targetLinkTestFolder, { recursive: true }); + await Promises.rm(targetLinkTestFolder); } // Copy with `preserveSymlinks: false` and verify result diff --git a/src/vs/code/buildfile.js b/src/vs/code/buildfile.js deleted file mode 100644 index df7ba33b058..00000000000 --- a/src/vs/code/buildfile.js +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -const { createModuleDescription } = require('../base/buildfile'); - -exports.collectModules = function () { - return [ - createModuleDescription('vs/code/electron-main/main'), - createModuleDescription('vs/code/node/cli'), - createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), - createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain'), - createModuleDescription('vs/code/electron-browser/sharedProcess/sharedProcessMain'), - createModuleDescription('vs/platform/driver/node/driver'), - createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain') - ]; -}; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index db9be03aafe..0166a2b0e5d 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -91,7 +91,6 @@ import { ipcSharedProcessTunnelChannelName, ISharedProcessTunnelService } from ' import { SharedProcessTunnelService } from 'vs/platform/tunnel/node/sharedProcessTunnelService'; import { ipcSharedProcessWorkerChannelName, ISharedProcessWorkerConfiguration, ISharedProcessWorkerService } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerService } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorkerService'; -import { AssignmentService } from 'vs/platform/assignment/common/assignmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { isLinux } from 'vs/base/common/platform'; @@ -215,10 +214,10 @@ class SharedProcessMain extends Disposable { // Since user data can change very frequently across multiple // processes, we want a single process handling these operations. this._register(new DiskFileSystemProviderClient(mainProcessService.getChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME), { pathCaseSensitive: isLinux })), - Schemas.userData, + Schemas.vscodeUserData, logService )); - fileService.registerProvider(Schemas.userData, userDataFileSystemProvider); + fileService.registerProvider(Schemas.vscodeUserData, userDataFileSystemProvider); // Configuration const configurationService = this._register(new ConfigurationService(environmentService.settingsResource, fileService)); @@ -259,9 +258,6 @@ class SharedProcessMain extends Disposable { const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); services.set(IExtensionRecommendationNotificationService, new ExtensionRecommendationNotificationServiceChannelClient(this.server.getChannel('extensionRecommendationNotification', activeWindowRouter))); - // Assignment Service (Experiment service w/out scorecards) - const assignmentService = new AssignmentService(this.configuration.machineId, configurationService, productService); - // Telemetry let telemetryService: ITelemetryService; const appenders: ITelemetryAppender[] = []; @@ -272,16 +268,7 @@ class SharedProcessMain extends Disposable { // Application Insights if (productService.aiConfig && productService.aiConfig.asimovKey) { - const testCollector = await assignmentService.getTreatment('telemetryMigration') ?? false; - const insiders = productService.quality !== 'stable'; - // Insiders send to both collector and vortex if assigned. - // Stable only send to one - if (insiders && testCollector) { - const collectorAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, testCollector, true); - this._register(toDisposable(() => collectorAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data - appenders.push(collectorAppender); - } - const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey, insiders ? false : testCollector, testCollector && insiders); + const appInsightsAppender = new AppInsightsAppender('monacoworkbench', null, productService.aiConfig.asimovKey); this._register(toDisposable(() => appInsightsAppender.flush())); // Ensure the AI appender is disposed so that it flushes remaining data appenders.push(appInsightsAppender); } @@ -291,7 +278,7 @@ class SharedProcessMain extends Disposable { commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, productService.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: getPiiPathsFromEnvironment(environmentService), - }, configurationService); + }, configurationService, productService); } else { telemetryService = NullTelemetryService; const nullAppender = NullAppender; @@ -302,7 +289,7 @@ class SharedProcessMain extends Disposable { services.set(ITelemetryService, telemetryService); // Custom Endpoint Telemetry - const customEndpointTelemetryService = new CustomEndpointTelemetryService(configurationService, telemetryService, loggerService, environmentService); + const customEndpointTelemetryService = new CustomEndpointTelemetryService(configurationService, telemetryService, loggerService, environmentService, productService); services.set(ICustomEndpointTelemetryService, customEndpointTelemetryService); // Extension Management diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index d83fb9abcd3..9f2927a92a4 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -107,7 +107,7 @@ // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; - if (data && isHighContrast && data.baseTheme !== 'hc-black') { + if (data && isHighContrast && data.baseTheme !== 'hc-black' && data.baseTheme !== 'hc-light') { data = undefined; } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 7792c22fd6e..d7459f90ea6 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -32,7 +32,6 @@ import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; -import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService'; import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; @@ -43,7 +42,7 @@ import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMai import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; -import { getResolvedShellEnv } from 'vs/platform/terminal/node/shellEnv'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust'; import { ExtensionUrlTrustService } from 'vs/platform/extensionManagement/node/extensionUrlTrustService'; import { IExtensionHostStarter, ipcExtensionHostStarterChannelName } from 'vs/platform/extensions/common/extensionHostStarter'; @@ -99,6 +98,7 @@ import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesHistoryMainService, WorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IWorkspacesManagementMainService, WorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; +import { CredentialsNativeMainService } from 'vs/platform/credentials/electron-main/credentialsMainService'; /** * The main VS Code application. There will only ever be one instance, @@ -195,8 +195,37 @@ export class CodeApplication extends Disposable { return false; }; + const isAllowedWebviewRequest = (uri: URI, details: Electron.OnBeforeRequestListenerDetails): boolean => { + // Only restrict top level page of webviews: index.html + if (uri.path !== '/index.html') { + return true; + } + + const frame = details.frame; + if (!frame || !this.windowsMainService) { + return false; + } + + // Check to see if the request comes from one of the main editor windows. + for (const window of this.windowsMainService.getWindows()) { + if (window.win) { + if (frame.processId === window.win.webContents.mainFrame.processId) { + return true; + } + } + } + + return false; + }; + session.defaultSession.webRequest.onBeforeRequest((details, callback) => { const uri = URI.parse(details.url); + if (uri.scheme === Schemas.vscodeWebview) { + if (!isAllowedWebviewRequest(uri, details)) { + this.logService.error('Blocked vscode-webview request', details.url); + return callback({ cancel: true }); + } + } if (uri.scheme === Schemas.vscodeFileResource) { if (!isAllowedVsCodeFileRequest(details)) { @@ -246,7 +275,7 @@ export class CodeApplication extends Disposable { //#region Code Cache - type SessionWithCodeCachePathSupport = typeof Session & { + type SessionWithCodeCachePathSupport = Session & { /** * Sets code cache directory. By default, the directory will be `Code Cache` under * the respective user data folder. @@ -603,7 +632,7 @@ export class CodeApplication extends Disposable { services.set(INativeHostMainService, new SyncDescriptor(NativeHostMainService, [sharedProcess])); // Credentials - services.set(ICredentialsMainService, new SyncDescriptor(CredentialsMainService, [false])); + services.set(ICredentialsMainService, new SyncDescriptor(CredentialsNativeMainService)); // Webview Manager services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); @@ -1059,11 +1088,11 @@ export class CodeApplication extends Disposable { // Telemetry type SharedProcessErrorClassification = { - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - visible: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - shuttingdown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of shared process crash to understand the nature of the crash better.' }; + visible: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'Whether shared process window was visible or not.' }; + shuttingdown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'Whether the application is shutting down when the crash happens.' }; }; type SharedProcessErrorEvent = { type: WindowError; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 4ba46359de9..6bed7a06fee 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -220,6 +220,7 @@ class CodeMain { environmentMainService.logsPath, environmentMainService.globalStorageHome.fsPath, environmentMainService.workspaceStorageHome.fsPath, + environmentMainService.localHistoryHome.fsPath, environmentMainService.backupHome ].map(path => path ? FSPromises.mkdir(path, { recursive: true }) : undefined)), diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 6985644c23c..f761fe0f599 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -107,7 +107,7 @@ // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts const isHighContrast = configuration.colorScheme.highContrast && configuration.autoDetectHighContrast; - if (data && isHighContrast && data.baseTheme !== 'hc-black') { + if (data && isHighContrast && data.baseTheme !== 'hc-black' && data.baseTheme !== 'hc-light') { data = undefined; } diff --git a/src/vs/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index 5af58c15fd4..cf65aabb60f 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -213,6 +213,7 @@ function getExtraEditorClassName(): string { if (browser.isSafari) { // See https://github.com/microsoft/vscode/issues/108822 extra += 'no-minimap-shadow '; + extra += 'enable-user-select '; } if (platform.isMacintosh) { extra += 'mac '; diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index e5d233afcfe..dacc3b77a7c 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -10,7 +10,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; import { IMouseTarget, IMouseTargetViewZoneData, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom'; +import { ClientCoordinates, EditorMouseEvent, EditorMouseEventFactory, GlobalEditorPointerMoveMonitor, createEditorPagePosition, createCoordinatesRelativeToEditor } from 'vs/editor/browser/editorDom'; import { ViewController } from 'vs/editor/browser/view/viewController'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { Position } from 'vs/editor/common/core/position'; @@ -40,6 +40,7 @@ export function createMouseMoveEventMerger(mouseTargetFactory: MouseTargetFactor export interface IPointerHandlerHelper { viewDomNode: HTMLElement; linesContentDomNode: HTMLElement; + viewLinesDomNode: HTMLElement; focusTextArea(): void; dispatchTextAreaEvent(event: CustomEvent): void; @@ -104,7 +105,17 @@ export class MouseHandler extends ViewEventHandler { this._register(mouseEvents.onMouseLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e))); - this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e))); + // `pointerdown` events can't be used to determine if there's a double click, or triple click + // because their `e.detail` is always 0. + // We will therefore save the pointer id for the mouse and then reuse it in the `mousedown` event + // for `element.setPointerCapture`. + let mousePointerId: number = 0; + this._register(mouseEvents.onPointerDown(this.viewHelper.viewDomNode, (e, pointerType, pointerId) => { + if (pointerType === 'mouse') { + mousePointerId = pointerId; + } + })); + this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e, mousePointerId))); const onMouseWheel = (browserEvent: IMouseWheelEvent) => { this.viewController.emitMouseWheel(browserEvent); @@ -232,7 +243,7 @@ export class MouseHandler extends ViewEventHandler { }); } - public _onMouseDown(e: EditorMouseEvent): void { + public _onMouseDown(e: EditorMouseEvent, pointerId: number): void { const t = this._createMouseTarget(e, true); const targetIsContent = (t.type === MouseTargetType.CONTENT_TEXT || t.type === MouseTargetType.CONTENT_EMPTY); @@ -254,7 +265,7 @@ export class MouseHandler extends ViewEventHandler { if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) { focus(); - this._mouseDownOperation.start(t.type, e); + this._mouseDownOperation.start(t.type, e, pointerId); } else if (targetIsGutter) { // Do not steal focus @@ -263,7 +274,7 @@ export class MouseHandler extends ViewEventHandler { const viewZoneData = t.detail; if (this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) { focus(); - this._mouseDownOperation.start(t.type, e); + this._mouseDownOperation.start(t.type, e, pointerId); e.preventDefault(); } } else if (targetIsWidget && this.viewHelper.shouldSuppressMouseDownOnWidget(t.detail)) { @@ -290,7 +301,7 @@ class MouseDownOperation extends Disposable { private readonly _createMouseTarget: (e: EditorMouseEvent, testEventTarget: boolean) => IMouseTarget; private readonly _getMouseColumn: (e: EditorMouseEvent) => number; - private readonly _mouseMoveMonitor: GlobalEditorMouseMoveMonitor; + private readonly _mouseMoveMonitor: GlobalEditorPointerMoveMonitor; private readonly _onScrollTimeout: TimeoutTimer; private readonly _mouseState: MouseDownState; @@ -312,7 +323,7 @@ class MouseDownOperation extends Disposable { this._createMouseTarget = createMouseTarget; this._getMouseColumn = getMouseColumn; - this._mouseMoveMonitor = this._register(new GlobalEditorMouseMoveMonitor(this._viewHelper.viewDomNode)); + this._mouseMoveMonitor = this._register(new GlobalEditorPointerMoveMonitor(this._viewHelper.viewDomNode)); this._onScrollTimeout = this._register(new TimeoutTimer()); this._mouseState = new MouseDownState(); @@ -333,7 +344,7 @@ class MouseDownOperation extends Disposable { this._lastMouseEvent = e; this._mouseState.setModifiers(e); - const position = this._findMousePosition(e, true); + const position = this._findMousePosition(e, false); if (!position) { // Ignoring because position is unknown return; @@ -349,7 +360,7 @@ class MouseDownOperation extends Disposable { } } - public start(targetType: MouseTargetType, e: EditorMouseEvent): void { + public start(targetType: MouseTargetType, e: EditorMouseEvent, pointerId: number): void { this._lastMouseEvent = e; this._mouseState.setStartedOnLineNumbers(targetType === MouseTargetType.GUTTER_LINE_NUMBERS); @@ -382,12 +393,13 @@ class MouseDownOperation extends Disposable { this._isActive = true; this._mouseMoveMonitor.startMonitoring( - e.target, + this._viewHelper.viewLinesDomNode, + pointerId, e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), (browserEvent?: MouseEvent | KeyboardEvent) => { - const position = this._findMousePosition(this._lastMouseEvent!, true); + const position = this._findMousePosition(this._lastMouseEvent!, false); if (browserEvent && browserEvent instanceof KeyboardEvent) { // cancel @@ -412,7 +424,8 @@ class MouseDownOperation extends Disposable { if (!this._isActive) { this._isActive = true; this._mouseMoveMonitor.startMonitoring( - e.target, + this._viewHelper.viewLinesDomNode, + pointerId, e.buttons, createMouseMoveEventMerger(null), (e) => this._onMouseDownThenMove(e), diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index f09a5765c0d..b584826c7bf 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -26,7 +26,7 @@ export class PointerEventHandler extends MouseHandler { this._register(Gesture.addTarget(this.viewHelper.linesContentDomNode)); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e))); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e))); - this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e: MouseEvent) => this._onContextMenu(new EditorMouseEvent(e, this.viewHelper.viewDomNode), false))); + this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e: MouseEvent) => this._onContextMenu(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode), false))); this._lastPointerType = 'mouse'; @@ -50,7 +50,7 @@ export class PointerEventHandler extends MouseHandler { createMouseMoveEventMerger(this.mouseTargetFactory), MouseHandler.MOUSE_MOVE_MINIMUM_TIME)); this._register(pointerEvents.onPointerUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e))); this._register(pointerEvents.onPointerLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e))); - this._register(pointerEvents.onPointerDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e))); + this._register(pointerEvents.onPointerDown(this.viewHelper.viewDomNode, (e, pointerId) => this._onMouseDown(e, pointerId))); } private onTap(event: GestureEvent): void { @@ -60,7 +60,7 @@ export class PointerEventHandler extends MouseHandler { event.preventDefault(); this.viewHelper.focusTextArea(); - const target = this._createMouseTarget(new EditorMouseEvent(event, this.viewHelper.viewDomNode), false); + const target = this._createMouseTarget(new EditorMouseEvent(event, false, this.viewHelper.viewDomNode), false); if (target.position) { // this.viewController.moveTo(target.position); @@ -88,12 +88,12 @@ export class PointerEventHandler extends MouseHandler { } } - public override _onMouseDown(e: EditorMouseEvent): void { + public override _onMouseDown(e: EditorMouseEvent, pointerId: number): void { if ((e.browserEvent as any).pointerType === 'touch') { return; } - super._onMouseDown(e); + super._onMouseDown(e, pointerId); } } @@ -106,7 +106,7 @@ class TouchHandler extends MouseHandler { this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e))); this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e))); - this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e: MouseEvent) => this._onContextMenu(new EditorMouseEvent(e, this.viewHelper.viewDomNode), false))); + this._register(dom.addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e: MouseEvent) => this._onContextMenu(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode), false))); } private onTap(event: GestureEvent): void { @@ -114,7 +114,7 @@ class TouchHandler extends MouseHandler { this.viewHelper.focusTextArea(); - const target = this._createMouseTarget(new EditorMouseEvent(event, this.viewHelper.viewDomNode), false); + const target = this._createMouseTarget(new EditorMouseEvent(event, false, this.viewHelper.viewDomNode), false); if (target.position) { // Send the tap event also to the - - - - diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index a2c493f13af..d13e5ba07aa 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -544,6 +544,60 @@ suite('TextAreaInput', () => { assert.deepStrictEqual(actualResultingState, recorded.final); }); + test('macOS - Chrome - pressing quotes on US Intl', async () => { + // macOS, US International - PC, press ', ', ; + const recorded: IRecorded = { + env: { OS: OperatingSystem.Macintosh, browser: { isAndroid: false, isFirefox: false, isChrome: true, isSafari: false } }, + initial: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, + events: [ + { timeStamp: 0.00, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: false, key: 'Dead', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false }, + { timeStamp: 2.80, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'compositionstart', data: '' }, + { timeStamp: 3.10, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 3.20, state: { value: 'aaaa', selectionStart: 2, selectionEnd: 2, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' }, + { timeStamp: 3.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 71.90, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 222, location: 0, metaKey: false, repeat: false, shiftKey: false }, + { timeStamp: 144.00, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false }, + { timeStamp: 146.20, state: { value: 'aa\'aa', selectionStart: 2, selectionEnd: 3, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 146.40, state: { value: 'aa\'aa', selectionStart: 2, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' }, + { timeStamp: 146.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 146.80, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionend', data: '\'' }, + { timeStamp: 147.20, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionstart', data: '' }, + { timeStamp: 147.20, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'beforeinput', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 147.70, state: { value: 'aa\'aa', selectionStart: 3, selectionEnd: 3, selectionDirection: 'none' }, type: 'compositionupdate', data: '\'' }, + { timeStamp: 148.20, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'input', data: '\'', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 208.10, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Quote', ctrlKey: false, isComposing: true, key: 'Dead', keyCode: 222, location: 0, metaKey: false, repeat: false, shiftKey: false }, + { timeStamp: 323.70, state: { value: 'aa\'\'aa', selectionStart: 4, selectionEnd: 4, selectionDirection: 'none' }, type: 'keydown', altKey: false, charCode: 0, code: 'Semicolon', ctrlKey: false, isComposing: true, key: ';', keyCode: 229, location: 0, metaKey: false, repeat: false, shiftKey: false }, + { timeStamp: 325.70, state: { value: 'aa\'\'aa', selectionStart: 3, selectionEnd: 4, selectionDirection: 'none' }, type: 'beforeinput', data: '\';', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 325.80, state: { value: 'aa\'\'aa', selectionStart: 3, selectionEnd: 4, selectionDirection: 'none' }, type: 'compositionupdate', data: '\';' }, + { timeStamp: 326.30, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'input', data: '\';', inputType: 'insertCompositionText', isComposing: true }, + { timeStamp: 326.30, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'compositionend', data: '\';' }, + { timeStamp: 428.00, state: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, type: 'keyup', altKey: false, charCode: 0, code: 'Semicolon', ctrlKey: false, isComposing: false, key: ';', keyCode: 186, location: 0, metaKey: false, repeat: false, shiftKey: false } + ], + final: { value: 'aa\'\';aa', selectionStart: 5, selectionEnd: 5, selectionDirection: 'none' }, + }; + + const actualOutgoingEvents = await simulateInteraction(recorded); + assert.deepStrictEqual(actualOutgoingEvents, ([ + { type: "compositionStart", data: "" }, + { type: "type", text: "'", replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionUpdate", data: "'" }, + { type: "type", text: "'", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionUpdate", data: "'" }, + { type: "type", text: "'", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionEnd" }, + { type: "compositionStart", data: "" }, + { type: "type", text: "'", replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionUpdate", data: "'" }, + { type: "type", text: "';", replacePrevCharCnt: 1, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionUpdate", data: "';" }, + { type: "type", text: "';", replacePrevCharCnt: 2, replaceNextCharCnt: 0, positionDelta: 0 }, + { type: "compositionEnd" } + ])); + + const actualResultingState = interpretTypeEvents(recorded.env.OS, recorded.env.browser, recorded.initial, actualOutgoingEvents); + assert.deepStrictEqual(actualResultingState, recorded.final); + }); + test('macOS - Chrome - inserting emoji using ctrl+cmd+space', async () => { // macOS, English, press ctrl+cmd+space, and then pick an emoji using the mouse // See https://github.com/microsoft/vscode/issues/4271 diff --git a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts index dec77cd99af..644f7c20019 100644 --- a/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts +++ b/src/vs/editor/test/browser/services/decorationRenderOptions.test.ts @@ -93,7 +93,7 @@ suite('Decoration Render Options', () => { const expected = [ '.vs-dark.monaco-editor .ced-example-4::after, .hc-black.monaco-editor .ced-example-4::after {color:#444444 !important;}', '.vs-dark.monaco-editor .ced-example-1, .hc-black.monaco-editor .ced-example-1 {color:#000000 !important;}', - '.vs.monaco-editor .ced-example-1 {color:#FF00FF !important;}', + '.vs.monaco-editor .ced-example-1, .hc-light.monaco-editor .ced-example-1 {color:#FF00FF !important;}', '.monaco-editor .ced-example-1 {color:#ff0000 !important;}' ].join('\n'); assert.strictEqual(readStyleSheet(styleSheet), expected); diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index d5c8ca95932..6bbab59a764 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -14,6 +14,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; @@ -25,6 +26,7 @@ import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestConfiguration } from 'vs/editor/test/browser/config/testConfiguration'; import { TestCodeEditorService, TestCommandService } from 'vs/editor/test/browser/editorTestServices'; import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService'; +import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEditorWorkerService'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; import { instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; @@ -172,6 +174,7 @@ export function createCodeEditorServices(disposables: DisposableStore, services: define(IAccessibilityService, TestAccessibilityService); define(IClipboardService, TestClipboardService); + define(IEditorWorkerService, TestEditorWorkerService); defineInstance(IOpenerService, NullOpenerService); define(INotificationService, TestNotificationService); define(IDialogService, TestDialogService); @@ -190,7 +193,7 @@ export function createCodeEditorServices(disposables: DisposableStore, services: define(ILanguageFeatureDebounceService, LanguageFeatureDebounceService); define(ILanguageFeaturesService, LanguageFeaturesService); - const instantiationService = new TestInstantiationService(services); + const instantiationService = new TestInstantiationService(services, true); disposables.add(toDisposable(() => { for (const id of serviceIdentifiers) { const instanceOrDescriptor = services.get(id); diff --git a/src/vs/editor/test/browser/view/viewLayer.test.ts b/src/vs/editor/test/browser/view/viewLayer.test.ts index eba81ff74ba..71261952b84 100644 --- a/src/vs/editor/test/browser/view/viewLayer.test.ts +++ b/src/vs/editor/test/browser/view/viewLayer.test.ts @@ -324,7 +324,7 @@ suite('RenderedLinesCollection onLineChanged', () => { new TestLine('old8'), new TestLine('old9') ]); - let actualPinged = col.onLinesChanged(changedLineNumber, changedLineNumber); + let actualPinged = col.onLinesChanged(changedLineNumber, 1); assert.deepStrictEqual(actualPinged, expectedPinged); assertState(col, expectedState); } diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index b873efdcb16..e2d59071382 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -105,6 +105,7 @@ suite('LanguageSelector', function () { assert.strictEqual(score('javascript', obj.uri, obj.langId, true, undefined), 10); assert.strictEqual(score('javascript', obj.uri, obj.langId, true, obj.notebookType), 10); assert.strictEqual(score({ notebookType: 'fooBook' }, obj.uri, obj.langId, true, obj.notebookType), 10); + assert.strictEqual(score({ notebookType: 'fooBook', language: 'javascript', scheme: 'file' }, obj.uri, obj.langId, true, obj.notebookType), 10); assert.strictEqual(score({ notebookType: 'fooBook', language: '*' }, obj.uri, obj.langId, true, obj.notebookType), 10); assert.strictEqual(score({ notebookType: '*', language: '*' }, obj.uri, obj.langId, true, obj.notebookType), 5); assert.strictEqual(score({ notebookType: '*', language: 'javascript' }, obj.uri, obj.langId, true, obj.notebookType), 10); diff --git a/src/vs/editor/test/common/services/testEditorWorkerService.ts b/src/vs/editor/test/common/services/testEditorWorkerService.ts new file mode 100644 index 00000000000..ae2fd98b2ca --- /dev/null +++ b/src/vs/editor/test/common/services/testEditorWorkerService.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/editor/common/core/range'; +import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; +import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages'; +import { IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer'; + +export class TestEditorWorkerService implements IEditorWorkerService { + + declare readonly _serviceBrand: undefined; + + canComputeUnicodeHighlights(uri: URI): boolean { return false; } + async computedUnicodeHighlights(uri: URI): Promise { return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; } + async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { return null; } + canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; } + async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return null; } + async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined): Promise { return undefined; } + canComputeWordRanges(resource: URI): boolean { return false; } + async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { return null; } + canNavigateValueSet(resource: URI): boolean { return false; } + async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } +} diff --git a/src/vs/editor/test/common/testTextModel.ts b/src/vs/editor/test/common/testTextModel.ts index 89e852df76d..1d12991f5a2 100644 --- a/src/vs/editor/test/common/testTextModel.ts +++ b/src/vs/editor/test/common/testTextModel.ts @@ -109,7 +109,7 @@ export function createModelServices(disposables: DisposableStore, services: Serv define(ILanguageFeaturesService, LanguageFeaturesService); define(IModelService, ModelService); - const instantiationService = new TestInstantiationService(services); + const instantiationService = new TestInstantiationService(services, true); disposables.add(toDisposable(() => { for (const id of serviceIdentifiers) { const instanceOrDescriptor = services.get(id); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 320574897a1..8dd03f9c013 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -465,8 +465,8 @@ suite('viewLineRenderer.renderLine', () => { const expectedOutput = [ 'var', - '\u00a0קודמות\u00a0=\u00a0', - '"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"', + '\u00a0קודמות\u00a0=\u00a0', + '"מיותר\u00a0קודמות\u00a0צ\'ט\u00a0של,\u00a0אם\u00a0לשון\u00a0העברית\u00a0שינויים\u00a0ויש,\u00a0אם"', ';' ].join(''); @@ -496,6 +496,63 @@ suite('viewLineRenderer.renderLine', () => { assert.strictEqual(_actual.containsRTL, true); }); + test('issue #137036: Issue in RTL languages in recent versions', () => { + const lineText = ''; + + const lineParts = createViewLineTokens([ + createPart(1, 2), + createPart(7, 3), + createPart(8, 4), + createPart(13, 5), + createPart(14, 4), + createPart(23, 6), + createPart(24, 2), + createPart(31, 4), + createPart(33, 2), + createPart(39, 3), + createPart(40, 2), + ]); + + const expectedOutput = [ + '<', + 'option', + '\u00a0', + 'value', + '=', + '"العربية"', + '>', + 'العربية', + '</', + 'option', + '>', + ].join(''); + + const _actual = renderViewLine(new RenderLineInput( + false, + true, + lineText, + false, + false, + true, + 0, + lineParts, + [], + 4, + 0, + 10, + 10, + 10, + -1, + 'none', + false, + false, + null + )); + + assert.strictEqual(_actual.html, '' + expectedOutput + ''); + assert.strictEqual(_actual.containsRTL, true); + }); + test('issue #6885: Splits large tokens', () => { // 1 1 1 // 1 2 3 4 5 6 7 8 9 0 1 2 @@ -681,7 +738,7 @@ suite('viewLineRenderer.renderLine', () => { const lineText = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.'; const lineParts = createViewLineTokens([createPart(lineText.length, 1)]); const expectedOutput = [ - 'את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.' + 'את\u00a0גרמנית\u00a0בהתייחסות\u00a0שמו,\u00a0שנתי\u00a0המשפט\u00a0אל\u00a0חפש,\u00a0אם\u00a0כתב\u00a0אחרים\u00a0ולחבר.\u00a0של\u00a0התוכן\u00a0אודות\u00a0בויקיפדיה\u00a0כלל,\u00a0של\u00a0עזרה\u00a0כימיה\u00a0היא.\u00a0על\u00a0עמוד\u00a0יוצרים\u00a0מיתולוגיה\u00a0סדר,\u00a0אם\u00a0שכל\u00a0שתפו\u00a0לעברית\u00a0שינויים,\u00a0אם\u00a0שאלות\u00a0אנגלית\u00a0עזה.\u00a0שמות\u00a0בקלות\u00a0מה\u00a0סדר.' ]; const actual = renderViewLine(new RenderLineInput( false, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 93202c746d5..1f5bab874d3 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1015,7 +1015,7 @@ declare namespace monaco.editor { */ export function registerCommand(id: string, handler: (accessor: any, ...args: any[]) => void): IDisposable; - export type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black'; + export type BuiltinTheme = 'vs' | 'vs-dark' | 'hc-black' | 'hc-light'; export interface IStandaloneThemeData { base: BuiltinTheme; @@ -1185,7 +1185,7 @@ declare namespace monaco.editor { maxTokenizationLineLength?: number; /** * Theme to be used for rendering. - * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black', 'hc-light'. * You can create custom themes via `monaco.editor.defineTheme`. * To switch a theme, use `monaco.editor.setTheme`. * **NOTE**: The theme might be overwritten if the OS is in high contrast mode, unless `autoDetectHighContrast` is set to false. @@ -1218,7 +1218,7 @@ declare namespace monaco.editor { language?: string; /** * Initial theme to be used for rendering. - * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black', 'hc-light. * You can create custom themes via `monaco.editor.defineTheme`. * To switch a theme, use `monaco.editor.setTheme`. * **NOTE**: The theme might be overwritten if the OS is in high contrast mode, unless `autoDetectHighContrast` is set to false. @@ -1249,7 +1249,7 @@ declare namespace monaco.editor { export interface IStandaloneDiffEditorConstructionOptions extends IDiffEditorConstructionOptions { /** * Initial theme to be used for rendering. - * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black', 'hc-light. * You can create custom themes via `monaco.editor.defineTheme`. * To switch a theme, use `monaco.editor.setTheme`. * **NOTE**: The theme might be overwritten if the OS is in high contrast mode, unless `autoDetectHighContrast` is set to false. @@ -1686,6 +1686,7 @@ declare namespace monaco.editor { export interface BracketPairColorizationOptions { enabled: boolean; + independentColorPoolPerBracketType: boolean; } export interface ITextModelUpdateOptions { @@ -3795,13 +3796,21 @@ declare namespace monaco.editor { cycle?: boolean; } + export type QuickSuggestionsValue = 'on' | 'inline' | 'off'; + /** * Configuration options for quick suggestions */ export interface IQuickSuggestionsOptions { - other?: boolean; - comments?: boolean; - strings?: boolean; + other?: boolean | QuickSuggestionsValue; + comments?: boolean | QuickSuggestionsValue; + strings?: boolean | QuickSuggestionsValue; + } + + export interface InternalQuickSuggestionsOptions { + readonly other: QuickSuggestionsValue; + readonly comments: QuickSuggestionsValue; + readonly strings: QuickSuggestionsValue; } export type LineNumbersType = 'on' | 'off' | 'relative' | 'interval' | ((lineNumber: number) => string); @@ -3974,6 +3983,10 @@ declare namespace monaco.editor { * Enable or disable bracket pair colorization. */ enabled?: boolean; + /** + * Use independent color pool per bracket type. + */ + independentColorPoolPerBracketType?: boolean; } export interface IGuidesOptions { @@ -4411,7 +4424,7 @@ declare namespace monaco.editor { parameterHints: IEditorOption>>; peekWidgetDefaultFocus: IEditorOption; definitionLinkOpensInPeek: IEditorOption; - quickSuggestions: IEditorOption; + quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; readOnly: IEditorOption; renameOnType: IEditorOption; @@ -4608,6 +4621,11 @@ declare namespace monaco.editor { * Placement preference for position, in order of preference. */ preference: ContentWidgetPositionPreference[]; + /** + * Placement preference when multiple view positions refer to the same (model) position. + * This plays a role when injected text is involved. + */ + positionAffinity?: PositionAffinity; } /** @@ -5395,7 +5413,6 @@ declare namespace monaco.editor { declare namespace monaco.languages { - export interface IRelativePattern { /** * A base file path to which this pattern will be matched against relatively. @@ -6258,8 +6275,24 @@ declare namespace monaco.languages { * The text to insert. * If the text contains a line break, the range must end at the end of a line. * If existing text should be replaced, the existing text must be a prefix of the text to insert. + * + * The text can also be a snippet. In that case, a preview with default parameters is shown. + * When accepting the suggestion, the full snippet is inserted. */ - readonly text: string; + readonly insertText: string | { + snippet: string; + }; + /** + * A text that is used to decide if this inline completion should be shown. + * An inline completion is shown if the text to replace is a subword of the filter text. + */ + readonly filterText?: string; + /** + * An optional array of additional text edits that are applied when + * selecting this completion. Edits must not overlap with the main edit + * nor with themselves. + */ + readonly additionalTextEdits?: editor.ISingleEditOperation[]; /** * The range to replace. * Must begin and end on the same line. @@ -6950,7 +6983,7 @@ declare namespace monaco.languages { export interface InlayHint { label: string | InlayHintLabelPart[]; tooltip?: string | IMarkdownString; - command?: Command; + textEdits?: TextEdit[]; position: IPosition; kind?: InlayHintKind; paddingLeft?: boolean; diff --git a/src/vs/platform/accessibility/browser/accessibilityService.ts b/src/vs/platform/accessibility/browser/accessibilityService.ts index d6afd7f3c1e..bbca82ccaaa 100644 --- a/src/vs/platform/accessibility/browser/accessibilityService.ts +++ b/src/vs/platform/accessibility/browser/accessibilityService.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { addDisposableListener } from 'vs/base/browser/dom'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { AccessibilitySupport, CONTEXT_ACCESSIBILITY_MODE_ENABLED, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; export class AccessibilityService extends Disposable implements IAccessibilityService { declare readonly _serviceBrand: undefined; @@ -17,21 +19,62 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe protected _accessibilitySupport = AccessibilitySupport.Unknown; protected readonly _onDidChangeScreenReaderOptimized = new Emitter(); + protected _configMotionReduced: 'auto' | 'on' | 'off'; + protected _systemMotionReduced: boolean; + protected readonly _onDidChangeReducedMotion = new Emitter(); + constructor( @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILayoutService private readonly _layoutService: ILayoutService, @IConfigurationService protected readonly _configurationService: IConfigurationService, ) { super(); this._accessibilityModeEnabledContext = CONTEXT_ACCESSIBILITY_MODE_ENABLED.bindTo(this._contextKeyService); + const updateContextKey = () => this._accessibilityModeEnabledContext.set(this.isScreenReaderOptimized()); this._register(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('editor.accessibilitySupport')) { updateContextKey(); this._onDidChangeScreenReaderOptimized.fire(); } + if (e.affectsConfiguration('workbench.reduceMotion')) { + this._configMotionReduced = this._configurationService.getValue('workbench.reduceMotion'); + this._onDidChangeReducedMotion.fire(); + } })); updateContextKey(); - this.onDidChangeScreenReaderOptimized(() => updateContextKey()); + this._register(this.onDidChangeScreenReaderOptimized(() => updateContextKey())); + + const reduceMotionMatcher = window.matchMedia(`(prefers-reduced-motion: reduce)`); + this._systemMotionReduced = reduceMotionMatcher.matches; + this._configMotionReduced = this._configurationService.getValue<'auto' | 'on' | 'off'>('workbench.reduceMotion'); + + this.initReducedMotionListeners(reduceMotionMatcher); + } + + private initReducedMotionListeners(reduceMotionMatcher: MediaQueryList) { + + if (!this._layoutService.hasContainer) { + // we can't use `ILayoutService.container` because the application + // doesn't have a single container + return; + } + + this._register(addDisposableListener(reduceMotionMatcher, 'change', () => { + this._systemMotionReduced = reduceMotionMatcher.matches; + if (this._configMotionReduced === 'auto') { + this._onDidChangeReducedMotion.fire(); + } + })); + + const updateRootClasses = () => { + const reduce = this.isMotionReduced(); + this._layoutService.container.classList.toggle('reduce-motion', reduce); + this._layoutService.container.classList.toggle('enable-motion', !reduce); + }; + + updateRootClasses(); + this._register(this.onDidChangeReducedMotion(() => updateRootClasses())); } get onDidChangeScreenReaderOptimized(): Event { @@ -43,14 +86,23 @@ export class AccessibilityService extends Disposable implements IAccessibilitySe return config === 'on' || (config === 'auto' && this._accessibilitySupport === AccessibilitySupport.Enabled); } - getAccessibilitySupport(): AccessibilitySupport { - return this._accessibilitySupport; + get onDidChangeReducedMotion(): Event { + return this._onDidChangeReducedMotion.event; + } + + isMotionReduced(): boolean { + const config = this._configMotionReduced; + return config === 'on' || (config === 'auto' && this._systemMotionReduced); } alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } + getAccessibilitySupport(): AccessibilitySupport { + return this._accessibilitySupport; + } + setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { if (this._accessibilitySupport === accessibilitySupport) { return; diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index aa1d1517de8..e156660fece 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -13,9 +13,11 @@ export interface IAccessibilityService { readonly _serviceBrand: undefined; readonly onDidChangeScreenReaderOptimized: Event; + readonly onDidChangeReducedMotion: Event; alwaysUnderlineAccessKeys(): Promise; isScreenReaderOptimized(): boolean; + isMotionReduced(): boolean; getAccessibilitySupport(): AccessibilitySupport; setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void; alert(message: string): void; diff --git a/src/vs/platform/accessibility/test/common/testAccessibilityService.ts b/src/vs/platform/accessibility/test/common/testAccessibilityService.ts index f51eb442d81..0789812b905 100644 --- a/src/vs/platform/accessibility/test/common/testAccessibilityService.ts +++ b/src/vs/platform/accessibility/test/common/testAccessibilityService.ts @@ -11,8 +11,10 @@ export class TestAccessibilityService implements IAccessibilityService { declare readonly _serviceBrand: undefined; onDidChangeScreenReaderOptimized = Event.None; + onDidChangeReducedMotion = Event.None; isScreenReaderOptimized(): boolean { return false; } + isMotionReduced(): boolean { return false; } alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.css b/src/vs/platform/actions/browser/menuEntryActionViewItem.css index 86fe628b520..b5b63dabb46 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.css +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.css @@ -11,7 +11,8 @@ background-size: 16px; } -.monaco-action-bar .action-item.menu-entry .action-label { +.monaco-action-bar .action-item.menu-entry .action-label, +.hc-light .monaco-action-bar .action-item.menu-entry .action-label { background-image: var(--menu-entry-icon-light); } @@ -39,7 +40,8 @@ background-size: 16px; } -.monaco-dropdown-with-default > .action-container.menu-entry > .action-label { +.monaco-dropdown-with-default > .action-container.menu-entry > .action-label, +.hc-light .monaco-dropdown-with-default > .action-container.menu-entry > .action-label { background-image: var(--menu-entry-icon-light); } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 5271efa36bb..5a4f3089d64 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -142,6 +142,7 @@ export class MenuId { static readonly TimelineItemContext = new MenuId('TimelineItemContext'); static readonly TimelineTitle = new MenuId('TimelineTitle'); static readonly TimelineTitleContext = new MenuId('TimelineTitleContext'); + static readonly TimelineFilterSubMenu = new MenuId('TimelineFilterSubMenu'); static readonly AccountsContext = new MenuId('AccountsContext'); static readonly PanelTitle = new MenuId('PanelTitle'); static readonly AuxiliaryBarTitle = new MenuId('AuxiliaryBarTitle'); diff --git a/src/vs/platform/configuration/common/configuration.ts b/src/vs/platform/configuration/common/configuration.ts index dcfdbd41441..6ed2b0c3ef8 100644 --- a/src/vs/platform/configuration/common/configuration.ts +++ b/src/vs/platform/configuration/common/configuration.ts @@ -290,3 +290,7 @@ export function getMigratedSettingValue(configurationService: IConfigurationS return setting.defaultValue!; } } + +export function getLanguageTagSettingPlainKey(settingKey: string) { + return settingKey.replace(/[\[\]]/g, ''); +} diff --git a/src/vs/platform/configuration/common/configurationModels.ts b/src/vs/platform/configuration/common/configurationModels.ts index fac7b51ce1f..8afa3b7f065 100644 --- a/src/vs/platform/configuration/common/configurationModels.ts +++ b/src/vs/platform/configuration/common/configurationModels.ts @@ -441,7 +441,7 @@ export class UserSettings extends Disposable { this._register(this.fileService.watch(this.userSettingsResource)); this._register(Event.any( Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.userSettingsResource)), - Event.filter(this.fileService.onDidRunOperation, e => (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.WRITE)) && extUri.isEqual(e.resource, userSettingsResource)) + Event.filter(this.fileService.onDidRunOperation, e => (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY) || e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.WRITE)) && extUri.isEqual(e.resource, userSettingsResource)) )(() => this._onDidChange.fire())); } diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 1aa57c19107..d6fecacc967 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as types from 'vs/base/common/types'; import * as nls from 'vs/nls'; +import { getLanguageTagSettingPlainKey } from 'vs/platform/configuration/common/configuration'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -292,10 +293,11 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (OVERRIDE_PROPERTY_REGEX.test(key)) { const defaultValue = { ...(this.configurationDefaultsOverrides.get(key)?.value || {}), ...overrides[key] }; this.configurationDefaultsOverrides.set(key, { source, value: defaultValue }); + const plainKey = getLanguageTagSettingPlainKey(key); const property: IRegisteredConfigurationPropertySchema = { type: 'object', default: defaultValue, - description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for {0} language.", key), + description: nls.localize('defaultLanguageConfiguration.description', "Configure settings to be overridden for the {0} language.", plainKey), $ref: resourceLanguageSettingsSchemaId, defaultDefaultValue: defaultValue, source: types.isString(source) ? undefined : source, diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts new file mode 100644 index 00000000000..6bed808b144 --- /dev/null +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICredentialsChangeEvent, ICredentialsMainService, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { isWindows } from 'vs/base/common/platform'; + +interface ChunkedPassword { + content: string; + hasNextChunk: boolean; +} + +export type KeytarModule = typeof import('keytar'); + +export abstract class BaseCredentialsMainService extends Disposable implements ICredentialsMainService { + + private static readonly MAX_PASSWORD_LENGTH = 2500; + private static readonly PASSWORD_CHUNK_SIZE = BaseCredentialsMainService.MAX_PASSWORD_LENGTH - 100; + declare readonly _serviceBrand: undefined; + + private _onDidChangePassword: Emitter = this._register(new Emitter()); + readonly onDidChangePassword = this._onDidChangePassword.event; + + protected _keytarCache: KeytarModule | undefined; + + constructor( + @ILogService protected readonly logService: ILogService, + ) { + super(); + } + + //#region abstract + + public abstract getSecretStoragePrefix(): Promise; + protected abstract withKeytar(): Promise; + + //#endregion + + async getPassword(service: string, account: string): Promise { + const keytar = await this.withKeytar(); + + const password = await keytar.getPassword(service, account); + if (password) { + try { + let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); + if (!content || !hasNextChunk) { + return password; + } + + let index = 1; + while (hasNextChunk) { + const nextChunk = await keytar.getPassword(service, `${account}-${index}`); + const result: ChunkedPassword = JSON.parse(nextChunk!); + content += result.content; + hasNextChunk = result.hasNextChunk; + index++; + } + + return content; + } catch { + return password; + } + } + + return password; + } + + async setPassword(service: string, account: string, password: string): Promise { + const keytar = await this.withKeytar(); + const MAX_SET_ATTEMPTS = 3; + + // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. + const setPasswordWithRetry = async (service: string, account: string, password: string) => { + let attempts = 0; + let error: any; + while (attempts < MAX_SET_ATTEMPTS) { + try { + await keytar.setPassword(service, account, password); + return; + } catch (e) { + error = e; + this.logService.warn('Error attempting to set a password: ', e); + attempts++; + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + // throw last error + throw error; + }; + + if (isWindows && password.length > BaseCredentialsMainService.MAX_PASSWORD_LENGTH) { + let index = 0; + let chunk = 0; + let hasNextChunk = true; + while (hasNextChunk) { + const passwordChunk = password.substring(index, index + BaseCredentialsMainService.PASSWORD_CHUNK_SIZE); + index += BaseCredentialsMainService.PASSWORD_CHUNK_SIZE; + hasNextChunk = password.length - index > 0; + + const content: ChunkedPassword = { + content: passwordChunk, + hasNextChunk: hasNextChunk + }; + + await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); + chunk++; + } + + } else { + await setPasswordWithRetry(service, account, password); + } + + this._onDidChangePassword.fire({ service, account }); + } + + async deletePassword(service: string, account: string): Promise { + const keytar = await this.withKeytar(); + + const didDelete = await keytar.deletePassword(service, account); + if (didDelete) { + this._onDidChangePassword.fire({ service, account }); + } + + return didDelete; + } + + async findPassword(service: string): Promise { + const keytar = await this.withKeytar(); + + return keytar.findPassword(service); + } + + async findCredentials(service: string): Promise> { + const keytar = await this.withKeytar(); + + return keytar.findCredentials(service); + } + + public clear(): Promise { + if (this._keytarCache instanceof InMemoryCredentialsProvider) { + return this._keytarCache.clear(); + } + + // We don't know how to properly clear Keytar because we don't know + // what services have stored credentials. For reference, a "service" is an extension. + // TODO: should we clear credentials for the built-in auth extensions? + return Promise.resolve(); + } +} diff --git a/src/vs/platform/credentials/electron-main/credentialsMainService.ts b/src/vs/platform/credentials/electron-main/credentialsMainService.ts new file mode 100644 index 00000000000..0bf232d2153 --- /dev/null +++ b/src/vs/platform/credentials/electron-main/credentialsMainService.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import { ILogService } from 'vs/platform/log/common/log'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; + +export class CredentialsNativeMainService extends BaseCredentialsMainService { + + constructor( + @ILogService logService: ILogService, + @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, + @IProductService private readonly productService: IProductService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + ) { + super(logService); + } + + // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the + // client would store the credentials. + public override async getSecretStoragePrefix() { return Promise.resolve(this.productService.urlProtocol); } + + protected async withKeytar(): Promise { + if (this._keytarCache) { + return this._keytarCache; + } + + if (this.environmentMainService.disableKeytar) { + this.logService.info('Keytar is disabled. Using in-memory credential store instead.'); + this._keytarCache = new InMemoryCredentialsProvider(); + return this._keytarCache; + } + + try { + this._keytarCache = await import('keytar'); + // Try using keytar to see if it throws or not. + await this._keytarCache.findCredentials('test-keytar-loads'); + } catch (e) { + this.windowsMainService.sendToFocused('vscode:showCredentialsError', e.message ?? e); + throw e; + } + return this._keytarCache; + } +} diff --git a/src/vs/platform/credentials/node/credentialsMainService.ts b/src/vs/platform/credentials/node/credentialsMainService.ts index 305a61e1526..f00f3532d00 100644 --- a/src/vs/platform/credentials/node/credentialsMainService.ts +++ b/src/vs/platform/credentials/node/credentialsMainService.ts @@ -3,147 +3,27 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsChangeEvent, ICredentialsMainService, InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; -import { Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { InMemoryCredentialsProvider } from 'vs/platform/credentials/common/credentials'; import { ILogService } from 'vs/platform/log/common/log'; -import { isWindows } from 'vs/base/common/platform'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; +import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; -interface ChunkedPassword { - content: string; - hasNextChunk: boolean; -} - -type KeytarModule = typeof import('keytar'); - -export class CredentialsMainService extends Disposable implements ICredentialsMainService { - - private static readonly MAX_PASSWORD_LENGTH = 2500; - private static readonly PASSWORD_CHUNK_SIZE = CredentialsMainService.MAX_PASSWORD_LENGTH - 100; - declare readonly _serviceBrand: undefined; - - private _onDidChangePassword: Emitter = this._register(new Emitter()); - readonly onDidChangePassword = this._onDidChangePassword.event; - - private _keytarCache: KeytarModule | undefined; - - // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the - // client would store the credentials. - public async getSecretStoragePrefix() { return `${this.productService.urlProtocol}${this.isRunningOnServer ? '-server' : ''}`; } +export class CredentialsWebMainService extends BaseCredentialsMainService { constructor( - private isRunningOnServer: boolean, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, @INativeEnvironmentService private readonly environmentMainService: INativeEnvironmentService, @IProductService private readonly productService: IProductService, ) { - super(); + super(logService); } - async getPassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); + // If the credentials service is running on the server, we add a suffix -server to differentiate from the location that the + // client would store the credentials. + public override async getSecretStoragePrefix() { return Promise.resolve(`${this.productService.urlProtocol}-server`); } - const password = await keytar.getPassword(service, account); - if (password) { - try { - let { content, hasNextChunk }: ChunkedPassword = JSON.parse(password); - if (!content || !hasNextChunk) { - return password; - } - - let index = 1; - while (hasNextChunk) { - const nextChunk = await keytar.getPassword(service, `${account}-${index}`); - const result: ChunkedPassword = JSON.parse(nextChunk!); - content += result.content; - hasNextChunk = result.hasNextChunk; - index++; - } - - return content; - } catch { - return password; - } - } - - return password; - } - - async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this.withKeytar(); - const MAX_SET_ATTEMPTS = 3; - - // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. - const setPasswordWithRetry = async (service: string, account: string, password: string) => { - let attempts = 0; - let error: any; - while (attempts < MAX_SET_ATTEMPTS) { - try { - await keytar.setPassword(service, account, password); - return; - } catch (e) { - error = e; - this.logService.warn('Error attempting to set a password: ', e); - attempts++; - await new Promise(resolve => setTimeout(resolve, 200)); - } - } - - // throw last error - throw error; - }; - - if (isWindows && password.length > CredentialsMainService.MAX_PASSWORD_LENGTH) { - let index = 0; - let chunk = 0; - let hasNextChunk = true; - while (hasNextChunk) { - const passwordChunk = password.substring(index, index + CredentialsMainService.PASSWORD_CHUNK_SIZE); - index += CredentialsMainService.PASSWORD_CHUNK_SIZE; - hasNextChunk = password.length - index > 0; - - const content: ChunkedPassword = { - content: passwordChunk, - hasNextChunk: hasNextChunk - }; - - await setPasswordWithRetry(service, chunk ? `${account}-${chunk}` : account, JSON.stringify(content)); - chunk++; - } - - } else { - await setPasswordWithRetry(service, account, password); - } - - this._onDidChangePassword.fire({ service, account }); - } - - async deletePassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); - - const didDelete = await keytar.deletePassword(service, account); - if (didDelete) { - this._onDidChangePassword.fire({ service, account }); - } - - return didDelete; - } - - async findPassword(service: string): Promise { - const keytar = await this.withKeytar(); - - return keytar.findPassword(service); - } - - async findCredentials(service: string): Promise> { - const keytar = await this.withKeytar(); - - return keytar.findCredentials(service); - } - - private async withKeytar(): Promise { + protected async withKeytar(): Promise { if (this._keytarCache) { return this._keytarCache; } @@ -159,25 +39,10 @@ export class CredentialsMainService extends Disposable implements ICredentialsMa // Try using keytar to see if it throws or not. await this._keytarCache.findCredentials('test-keytar-loads'); } catch (e) { - // We should still throw errors on desktop so that the user is prompted with the - // troubleshooting steps. - if (!this.isRunningOnServer) { - throw e; - } - this.logService.warn(`Switching to using in-memory credential store instead because Keytar failed to load: ${e.message}`); + this.logService.warn( + `Using the in-memory credential store as the operating system's credential store could not be accessed. Please see https://aka.ms/vscode-server-keyring on how to set this up. Details: ${e.message ?? e}`); this._keytarCache = new InMemoryCredentialsProvider(); } return this._keytarCache; } - - public clear(): Promise { - if (this._keytarCache instanceof InMemoryCredentialsProvider) { - return this._keytarCache.clear(); - } - - // We don't know how to properly clear Keytar because we don't know - // what services have stored credentials. For reference, a "service" is an extension. - // TODO: should we clear credentials for the built-in auth extensions? - return Promise.resolve(); - } } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 22d030e30f4..28a690c69bb 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -59,6 +59,7 @@ export interface IEnvironmentService { untitledWorkspacesHome: URI; globalStorageHome: URI; workspaceStorageHome: URI; + localHistoryHome: URI; cacheHome: URI; // --- settings sync diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 82dc3799130..ac02eadcbdb 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -87,10 +87,13 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get machineSettingsResource(): URI { return joinPath(URI.file(join(this.userDataPath, 'Machine')), 'settings.json'); } @memoize - get globalStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'globalStorage'); } + get globalStorageHome(): URI { return joinPath(this.appSettingsHome, 'globalStorage'); } @memoize - get workspaceStorageHome(): URI { return URI.joinPath(this.appSettingsHome, 'workspaceStorage'); } + get workspaceStorageHome(): URI { return joinPath(this.appSettingsHome, 'workspaceStorage'); } + + @memoize + get localHistoryHome(): URI { return joinPath(this.appSettingsHome, 'History'); } @memoize get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); } diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index 89177b4d0ee..6b7dfad6741 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -35,12 +35,12 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof watchDog.start === 'function', testErrorMessage('native-watchdog')); }); - test('node-pty', async () => { + (process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('node-pty', async () => { const nodePty = await import('node-pty'); assert.ok(typeof nodePty.spawn === 'function', testErrorMessage('node-pty')); }); - test('spdlog', async () => { + (process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('spdlog', async () => { const spdlog = await import('spdlog'); assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog')); assert.ok(typeof spdlog.version === 'number', testErrorMessage('spdlog')); @@ -109,7 +109,7 @@ flakySuite('Native Modules (all platforms)', () => { (!isWindows ? suite.skip : suite)('Native Modules (Windows)', () => { - test('windows-mutex', async () => { + (process.type === 'renderer' ? test.skip /* TODO@electron module is not context aware yet and thus cannot load in Electron renderer used by tests */ : test)('windows-mutex', async () => { const mutex = await import('windows-mutex'); assert.ok(mutex && typeof mutex.isActive === 'function', testErrorMessage('windows-mutex')); assert.ok(typeof mutex.isActive === 'function', testErrorMessage('windows-mutex')); diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 7f175cb3ab6..574a72fa436 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -10,15 +10,14 @@ import { canceled, getErrorMessage } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; -import { isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { DidUninstallExtensionEvent, ExtensionManagementError, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementParticipant, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOperation, InstallOptions, - InstallVSIXOptions, IExtensionsControlManifest, StatisticType, UninstallOptions, TargetPlatform, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode + InstallVSIXOptions, IExtensionsControlManifest, StatisticType, UninstallOptions, isTargetPlatformCompatible, TargetPlatformToString, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { areSameExtensions, ExtensionKey, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, getMaliciousExtensionsSet } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -138,7 +137,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl protected async installExtension(manifest: IExtensionManifest, extension: URI | IGalleryExtension, options: InstallOptions & InstallVSIXOptions): Promise { // only cache gallery extensions tasks if (!URI.isUri(extension)) { - let installExtensionTask = this.installingExtensions.get(new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key()); + let installExtensionTask = this.installingExtensions.get(ExtensionKey.create(extension).toString()); if (installExtensionTask) { this.logService.info('Extensions is already requested to install', extension.identifier.id); return installExtensionTask.waitUntilTaskIsFinished(); @@ -150,7 +149,7 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const installResults: (InstallExtensionResult & { local: ILocalExtension })[] = []; const installExtensionTask = this.createInstallExtensionTask(manifest, extension, options); if (!URI.isUri(extension)) { - this.installingExtensions.set(new ExtensionIdentifierWithVersion(installExtensionTask.identifier, manifest.version).key(), installExtensionTask); + this.installingExtensions.set(ExtensionKey.create(extension).toString(), installExtensionTask); } this._onInstallExtension.fire({ identifier: installExtensionTask.identifier, source: extension }); this.logService.info('Installing extension:', installExtensionTask.identifier.id); @@ -165,11 +164,12 @@ export abstract class AbstractExtensionManagementService extends Disposable impl const allDepsAndPackExtensionsToInstall = await this.getAllDepsAndPackExtensionsToInstall(installExtensionTask.identifier, manifest, !!options.installOnlyNewlyAddedFromExtensionPack, !!options.installPreReleaseVersion); for (const { gallery, manifest } of allDepsAndPackExtensionsToInstall) { installExtensionHasDependents = installExtensionHasDependents || !!manifest.extensionDependencies?.some(id => areSameExtensions({ id }, installExtensionTask.identifier)); - if (this.installingExtensions.has(new ExtensionIdentifierWithVersion(gallery.identifier, gallery.version).key())) { + const key = ExtensionKey.create(gallery).toString(); + if (this.installingExtensions.has(key)) { this.logService.info('Extension is already requested to install', gallery.identifier.id); } else { const task = this.createInstallExtensionTask(manifest, gallery, { ...options, donotIncludePackAndDependencies: true }); - this.installingExtensions.set(new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key(), task); + this.installingExtensions.set(key, task); this._onInstallExtension.fire({ identifier: task.identifier, source: gallery }); this.logService.info('Installing extension:', task.identifier.id); allInstallExtensionTasks.push({ task, manifest }); @@ -212,23 +212,22 @@ export abstract class AbstractExtensionManagementService extends Disposable impl // Install extensions in parallel and wait until all extensions are installed / failed await this.joinAllSettled(extensionsToInstall.map(async ({ task }) => { const startTime = new Date().getTime(); - const operation = isUndefined(options.operation) ? task.operation : options.operation; try { const local = await task.run(); await this.joinAllSettled(this.participants.map(participant => participant.postInstall(local, task.source, options, CancellationToken.None))); if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, undefined); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, undefined); // In web, report extension install statistics explicitly. In Desktop, statistics are automatically updated while downloading the VSIX. - if (isWeb && operation !== InstallOperation.Update) { + if (isWeb && task.operation !== InstallOperation.Update) { try { await this.galleryService.reportStatistic(local.manifest.publisher, local.manifest.name, local.manifest.version, StatisticType.Install); } catch (error) { /* ignore */ } } } - installResults.push({ local, identifier: task.identifier, operation, source: task.source }); + installResults.push({ local, identifier: task.identifier, operation: task.operation, source: task.source }); } catch (error) { if (!URI.isUri(task.source)) { - reportTelemetry(this.telemetryService, operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, error); + reportTelemetry(this.telemetryService, task.operation === InstallOperation.Update ? 'extensionGallery:update' : 'extensionGallery:install', getGalleryExtensionTelemetryData(task.source), new Date().getTime() - startTime, error); } this.logService.error('Error while installing the extension:', task.identifier.id); throw error; @@ -268,9 +267,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw error; } finally { /* Remove the gallery tasks from the cache */ - for (const { task, manifest } of allInstallExtensionTasks) { + for (const { task } of allInstallExtensionTasks) { if (!URI.isUri(task.source)) { - const key = new ExtensionIdentifierWithVersion(task.identifier, manifest.version).key(); + const key = ExtensionKey.create(task.source).toString(); if (!this.installingExtensions.delete(key)) { this.logService.warn('Installation task is not found in the cache', key); } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 948c48a937e..f82d65aa963 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -16,9 +16,9 @@ import { URI } from 'vs/base/common/uri'; import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, TargetPlatform, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getFallbackTargetPlarforms, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionInfo, IGalleryExtension, IGalleryExtensionAsset, IGalleryExtensionAssets, IGalleryExtensionVersion, InstallOperation, IQueryOptions, IExtensionsControlManifest, isNotWebExtensionInWebTargetPlatform, isTargetPlatformCompatible, ITranslation, SortBy, SortOrder, StatisticType, toTargetPlatform, WEB_EXTENSION_TAG, IExtensionQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { adoptToGalleryExtensionId, areSameExtensions, getGalleryExtensionId, getGalleryExtensionTelemetryData } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; @@ -238,6 +238,7 @@ type GalleryServiceQueryClassification = { readonly flags: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly sortBy: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly sortOrder: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + readonly pageNumber: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; 'isMeasurement': true }; readonly success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; readonly requestBodySize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -252,6 +253,7 @@ type QueryTelemetryData = { readonly filterTypes: string[]; readonly sortBy: string; readonly sortOrder: string; + readonly pageNumber: string; }; type GalleryServiceQueryEvent = QueryTelemetryData & { @@ -336,7 +338,8 @@ class Query { filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)), flags: this.state.flags, sortBy: String(this.sortBy), - sortOrder: String(this.sortOrder) + sortOrder: String(this.sortOrder), + pageNumber: String(this.pageNumber) }; } } @@ -765,7 +768,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi if (hasAllVersions) { const extensions: IGalleryExtension[] = []; for (const rawGalleryExtension of rawGalleryExtensions) { - const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, true); + const extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria); if (extension) { extensions.push(extension); } @@ -779,16 +782,22 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const rawGalleryExtension = rawGalleryExtensions[index]; const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const includePreRelease = isBoolean(criteria.includePreRelease) ? criteria.includePreRelease : !!criteria.includePreRelease.find(extensionIdentifierWithPreRelease => areSameExtensions(extensionIdentifierWithPreRelease, extensionIdentifier))?.includePreRelease; - let extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria, false); + let extension = await this.toGalleryExtensionWithCriteria(rawGalleryExtension, criteria); + if (!extension && criteria.compatible && criteria.targetPlatform === TargetPlatform.WEB) { + /** Skip if requested for a web-compatible extension and not found. + * All versions are not needed in case of web-compatible extension + */ + continue; + } if (!extension - /** Skip if the extension is a pre-release version but + /** Need all versions if the extension is a pre-release version but * - the query is to look for a release version or * - the extension has no release version * Get all versions to get or check the release version */ || (extension.properties.isPreReleaseVersion && (!includePreRelease || !extension.hasReleaseVersion)) /** - * Skip if the extension is a release version with a different target platform than requested and also has a pre-release version + * Need all versions if the extension is a release version with a different target platform than requested and also has a pre-release version * Because, this is a platform specific extension and can have a newer release version supporting this platform. * See https://github.com/microsoft/vscode/issues/139628 */ @@ -820,7 +829,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi return { extensions: result.sort((a, b) => a[0] - b[0]).map(([, extension]) => extension), total }; } - private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria, hasAllVersions: boolean): Promise { + private async toGalleryExtensionWithCriteria(rawGalleryExtension: IRawGalleryExtension, criteria: IExtensionCriteria): Promise { const extensionIdentifier = { id: getGalleryExtensionId(rawGalleryExtension.publisher.publisherName, rawGalleryExtension.extensionName), uuid: rawGalleryExtension.extensionId }; const version = criteria.versions?.find(extensionIdentifierWithVersion => areSameExtensions(extensionIdentifierWithVersion, extensionIdentifier))?.version; @@ -828,7 +837,7 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const allTargetPlatforms = getAllTargetPlatforms(rawGalleryExtension); const rawGalleryExtensionVersions = sortExtensionVersions(rawGalleryExtension.versions, criteria.targetPlatform); - if (criteria.compatible && hasAllVersions && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { + if (criteria.compatible && isNotWebExtensionInWebTargetPlatform(allTargetPlatforms, criteria.targetPlatform)) { return null; } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index ed76e7f46b7..e7bf250146b 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -10,45 +10,13 @@ import { IPager } from 'vs/base/common/paging'; import { Platform } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { adoptToGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const EXTENSION_IDENTIFIER_PATTERN = '^([a-z0-9A-Z][a-z0-9-A-Z]*)\\.([a-z0-9A-Z][a-z0-9-A-Z]*)$'; export const EXTENSION_IDENTIFIER_REGEX = new RegExp(EXTENSION_IDENTIFIER_PATTERN); export const WEB_EXTENSION_TAG = '__web_extension'; -const EXTENSION_IDENTIFIER_WITH_VERSION_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/; -export function getIdAndVersion(id: string): [string, string | undefined] { - const matches = EXTENSION_IDENTIFIER_WITH_VERSION_REGEX.exec(id); - if (matches && matches[1]) { - return [adoptToGalleryExtensionId(matches[1]), matches[2]]; - } - return [adoptToGalleryExtensionId(id), undefined]; -} - -export const enum TargetPlatform { - WIN32_X64 = 'win32-x64', - WIN32_IA32 = 'win32-ia32', - WIN32_ARM64 = 'win32-arm64', - - LINUX_X64 = 'linux-x64', - LINUX_ARM64 = 'linux-arm64', - LINUX_ARMHF = 'linux-armhf', - - ALPINE_X64 = 'alpine-x64', - ALPINE_ARM64 = 'alpine-arm64', - - DARWIN_X64 = 'darwin-x64', - DARWIN_ARM64 = 'darwin-arm64', - - WEB = 'web', - - UNIVERSAL = 'universal', - UNKNOWN = 'unknown', - UNDEFINED = 'undefined', -} - export function TargetPlatformToString(targetPlatform: TargetPlatform) { switch (targetPlatform) { case TargetPlatform.WIN32_X64: return 'Windows 64 bit'; @@ -266,9 +234,10 @@ export interface IGalleryMetadata { publisherId: string; publisherDisplayName: string; isPreReleaseVersion: boolean; + targetPlatform?: TargetPlatform; } -export type Metadata = Partial; +export type Metadata = Partial; export interface ILocalExtension extends IExtension { isMachineScoped: boolean; @@ -277,6 +246,7 @@ export interface ILocalExtension extends IExtension { installedTimestamp?: number; isPreReleaseVersion: boolean; preRelease: boolean; + updated: boolean; } export const enum SortBy { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts index 2fcfbce9cdd..dc43b23fe1f 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementCLIService.ts @@ -10,8 +10,8 @@ import { Schemas } from 'vs/base/common/network'; import { gt } from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { CLIOutput, getIdAndVersion, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { CLIOutput, IExtensionGalleryService, IExtensionManagementCLIService, IExtensionManagementService, IGalleryExtension, ILocalExtension, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions, getGalleryExtensionId, getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, EXTENSION_CATEGORIES, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -90,7 +90,7 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer const checkIfNotInstalled = (id: string, version?: string): boolean => { const installedExtension = installed.find(i => areSameExtensions(i.identifier, { id })); if (installedExtension) { - if (!version && !force) { + if (!force && (!version || (version === 'prerelease' && installedExtension.preRelease))) { output.log(localize('alreadyInstalled-checkAndUpdate', "Extension '{0}' v{1} is already installed. Use '--force' option to update to latest version or provide '@' to install a specific version, for example: '{2}@1.2.3'.", id, installedExtension.manifest.version, id)); return false; } @@ -101,6 +101,9 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer } return true; }; + const addInstallExtensionInfo = (id: string, version: string | undefined, isBuiltin: boolean) => { + installExtensionInfos.push({ id, version: version !== 'prerelease' ? version : undefined, installOptions: { ...installOptions, isBuiltin, installPreReleaseVersion: version === 'prerelease' || installOptions.installPreReleaseVersion } }); + }; const vsixs: URI[] = []; const installExtensionInfos: InstallExtensionInfo[] = []; for (const extension of extensions) { @@ -109,14 +112,14 @@ export class ExtensionManagementCLIService implements IExtensionManagementCLISer } else { const [id, version] = getIdAndVersion(extension); if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { ...installOptions, isBuiltin: false } }); + addInstallExtensionInfo(id, version, false); } } } for (const extension of builtinExtensionIds) { const [id, version] = getIdAndVersion(extension); if (checkIfNotInstalled(id, version)) { - installExtensionInfos.push({ id, version, installOptions: { ...installOptions, isBuiltin: true } }); + addInstallExtensionInfo(id, version, true); } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 3141cf69855..b6e72ec8140 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -9,8 +9,8 @@ import { cloneAndChange } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; import { DefaultURITransformer, IURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IExtensionsControlManifest, isTargetPlatformCompatible, TargetPlatform, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { DidUninstallExtensionEvent, IExtensionIdentifier, IExtensionManagementService, IExtensionTipsService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, InstallOptions, InstallVSIXOptions, IExtensionsControlManifest, isTargetPlatformCompatible, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI { return URI.revive(transformer ? transformer.transformIncoming(uri) : uri); @@ -113,7 +113,7 @@ export class ExtensionManagementChannelClient extends Disposable implements IExt typeof (thing).scheme === 'string'; } - private _targetPlatformPromise: Promise | undefined; + protected _targetPlatformPromise: Promise | undefined; getTargetPlatform(): Promise { if (!this._targetPlatformPromise) { this._targetPlatformPromise = this.channel.call('getTargetPlatform'); diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index c38d117ec20..236b6e63174 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -4,8 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { compareIgnoreCase } from 'vs/base/common/strings'; -import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, IExtensionsControlManifest } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifier, IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionIdentifier, IGalleryExtension, ILocalExtension, IExtensionsControlManifest, getTargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionIdentifier, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { isLinux, platform } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; +import { getErrorMessage } from 'vs/base/common/errors'; +import { ILogService } from 'vs/platform/log/common/log'; +import { arch } from 'vs/base/common/process'; export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean { if (a.uuid && b.uuid) { @@ -17,31 +23,52 @@ export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifi return compareIgnoreCase(a.id, b.id) === 0; } -export class ExtensionIdentifierWithVersion { +const ExtensionKeyRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)(-(.+))?$/; + +export class ExtensionKey { + + static create(extension: ILocalExtension | IGalleryExtension): ExtensionKey { + const version = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).manifest.version : (extension as IGalleryExtension).version; + const targetPlatform = (extension as ILocalExtension).manifest ? (extension as ILocalExtension).targetPlatform : (extension as IGalleryExtension).properties.targetPlatform; + return new ExtensionKey(extension.identifier, version, targetPlatform); + } + + static parse(key: string): ExtensionKey | null { + const matches = ExtensionKeyRegex.exec(key); + return matches && matches[1] && matches[2] ? new ExtensionKey({ id: matches[1] }, matches[2], matches[4] as TargetPlatform || undefined) : null; + } readonly id: string; - readonly uuid?: string; constructor( identifier: IExtensionIdentifier, - readonly version: string + readonly version: string, + readonly targetPlatform: TargetPlatform = TargetPlatform.UNDEFINED, ) { this.id = identifier.id; - this.uuid = identifier.uuid; } - key(): string { - return `${this.id}-${this.version}`; + toString(): string { + return `${this.id}-${this.version}${this.targetPlatform !== TargetPlatform.UNDEFINED ? `-${this.targetPlatform}` : ''}`; } equals(o: any): boolean { - if (!(o instanceof ExtensionIdentifierWithVersion)) { + if (!(o instanceof ExtensionKey)) { return false; } - return areSameExtensions(this, o) && this.version === o.version; + return areSameExtensions(this, o) && this.version === o.version && this.targetPlatform === o.targetPlatform; } } +const EXTENSION_IDENTIFIER_WITH_VERSION_REGEX = /^([^.]+\..+)@((prerelease)|(\d+\.\d+\.\d+(-.*)?))$/; +export function getIdAndVersion(id: string): [string, string | undefined] { + const matches = EXTENSION_IDENTIFIER_WITH_VERSION_REGEX.exec(id); + if (matches && matches[1]) { + return [adoptToGalleryExtensionId(matches[1]), matches[2]]; + } + return [adoptToGalleryExtensionId(id), undefined]; +} + export function getExtensionId(publisher: string, name: string): string { return `${publisher}.${name}`; } @@ -149,3 +176,30 @@ export function getExtensionDependencies(installedExtensions: ReadonlyArray { + if (!isLinux) { + return false; + } + let content: string | undefined; + try { + const fileContent = await fileService.readFile(URI.file('/etc/os-release')); + content = fileContent.value.toString(); + } catch (error) { + try { + const fileContent = await fileService.readFile(URI.file('/usr/lib/os-release')); + content = fileContent.value.toString(); + } catch (error) { + /* Ignore */ + logService.debug(`Error while getting the os-release file.`, getErrorMessage(error)); + } + } + return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; +} + +export async function computeTargetPlatform(fileService: IFileService, logService: ILogService): Promise { + const alpineLinux = await isAlpineLinux(fileService, logService); + const targetPlatform = getTargetPlatform(alpineLinux ? 'alpine' : platform, arch); + logService.debug('ComputeTargetPlatform:', targetPlatform); + return targetPlatform; +} diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index f7efcb113af..c37857b3dce 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -13,13 +13,11 @@ import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { Promises as FSPromises } from 'vs/base/node/pfs'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionGalleryService, IGalleryExtension, InstallOperation, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; -const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/; - export class ExtensionsDownloader extends Disposable { private readonly extensionsDownloadDir: URI; @@ -97,9 +95,9 @@ export class ExtensionsDownloader extends Disposable { const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true }); if (folderStat.children) { const toDelete: URI[] = []; - const all: [ExtensionIdentifierWithVersion, IFileStatWithMetadata][] = []; + const all: [ExtensionKey, IFileStatWithMetadata][] = []; for (const stat of folderStat.children) { - const extension = this.parse(stat.name); + const extension = ExtensionKey.parse(stat.name); if (extension) { all.push([extension, stat]); } @@ -124,11 +122,7 @@ export class ExtensionsDownloader extends Disposable { } private getName(extension: IGalleryExtension): string { - return this.cache ? `${new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase()}${extension.properties.targetPlatform !== TargetPlatform.UNDEFINED ? `-${extension.properties.targetPlatform}` : ''}` : generateUuid(); + return this.cache ? ExtensionKey.create(extension).toString().toLowerCase() : generateUuid(); } - private parse(name: string): ExtensionIdentifierWithVersion | null { - const matches = ExtensionIdVersionRegex.exec(name); - return matches && matches[1] && matches[2] ? new ExtensionIdentifierWithVersion({ id: matches[1] }, matches[2]) : null; - } } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index c522d9c1de2..0a0254ea806 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,11 +8,10 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { getErrorMessage } from 'vs/base/common/errors'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; -import { isLinux, isMacintosh, platform } from 'vs/base/common/platform'; -import { arch } from 'vs/base/common/process'; +import { isMacintosh } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; -import { isBoolean } from 'vs/base/common/types'; +import { isBoolean, isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; @@ -22,17 +21,17 @@ import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExtensionTask, IUninstallExtensionTask, joinErrors, UninstallExtensionTaskOptions } from 'vs/platform/extensionManagement/common/abstractExtensionManagementService'; import { - ExtensionManagementError, ExtensionManagementErrorCode, getTargetPlatform, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, - InstallVSIXOptions, Metadata, TargetPlatform + ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, + InstallVSIXOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; import { ExtensionsScanner, ILocalExtensionManifest } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; -import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -43,7 +42,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' interface InstallableExtension { zipPath: string; - identifierWithVersion: ExtensionIdentifierWithVersion; + key: ExtensionKey; metadata?: Metadata; } @@ -66,7 +65,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi ) { super(galleryService, telemetryService, logService, productService); const extensionLifecycle = this._register(instantiationService.createInstance(ExtensionsLifecycle)); - this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension))); + this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner, extension => extensionLifecycle.postUninstall(extension), this.getTargetPlatform())); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); const extensionsWatcher = this._register(new ExtensionsWatcher(this, fileService, environmentService, logService, uriIdentityService)); @@ -82,36 +81,11 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi private _targetPlatformPromise: Promise | undefined; getTargetPlatform(): Promise { if (!this._targetPlatformPromise) { - this._targetPlatformPromise = (async () => { - const isAlpineLinux = await this.isAlpineLinux(); - const targetPlatform = getTargetPlatform(isAlpineLinux ? 'alpine' : platform, arch); - this.logService.debug('ExtensionManagementService#TargetPlatform:', targetPlatform); - return targetPlatform; - })(); + this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); } return this._targetPlatformPromise; } - private async isAlpineLinux(): Promise { - if (!isLinux) { - return false; - } - let content: string | undefined; - try { - const fileContent = await this.fileService.readFile(URI.file('/etc/os-release')); - content = fileContent.value.toString(); - } catch (error) { - try { - const fileContent = await this.fileService.readFile(URI.file('/usr/lib/os-release')); - content = fileContent.value.toString(); - } catch (error) { - /* Ignore */ - this.logService.debug(`Error while getting the os-release file.`, getErrorMessage(error)); - } - } - return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; - } - async zip(extension: ILocalExtension): Promise { this.logService.trace('ExtensionManagementService#zip', extension.identifier.id); const files = await this.collectFiles(extension); @@ -216,11 +190,12 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi abstract class AbstractInstallExtensionTask extends AbstractExtensionTask implements IInstallExtensionTask { protected _operation = InstallOperation.Install; - get operation() { return this._operation; } + get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } constructor( readonly identifier: IExtensionIdentifier, readonly source: URI | IGalleryExtension, + protected readonly options: InstallOptions, protected readonly extensionsScanner: ExtensionsScanner, protected readonly logService: ILogService, ) { @@ -229,7 +204,7 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask { try { - const local = await this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion); + const local = await this.unsetUninstalledAndGetLocal(installableExtension.key); if (local) { return installableExtension.metadata ? this.extensionsScanner.saveMetadataForLocalExtension(local, { ...((local.manifest).__metadata || {}), ...installableExtension.metadata }) : local; } @@ -243,28 +218,28 @@ abstract class AbstractInstallExtensionTask extends AbstractExtensionTask { - const isUninstalled = await this.isUninstalled(identifierWithVersion); + protected async unsetUninstalledAndGetLocal(extensionKey: ExtensionKey): Promise { + const isUninstalled = await this.isUninstalled(extensionKey); if (!isUninstalled) { return null; } - this.logService.trace('Removing the extension from uninstalled list:', identifierWithVersion.id); + this.logService.trace('Removing the extension from uninstalled list:', extensionKey.id); // If the same version of extension is marked as uninstalled, remove it from there and return the local. - const local = await this.extensionsScanner.setInstalled(identifierWithVersion); - this.logService.info('Removed the extension from uninstalled list:', identifierWithVersion.id); + const local = await this.extensionsScanner.setInstalled(extensionKey); + this.logService.info('Removed the extension from uninstalled list:', extensionKey.id); return local; } - private async isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise { + private async isUninstalled(extensionId: ExtensionKey): Promise { const uninstalled = await this.extensionsScanner.getUninstalledExtensions(); - return !!uninstalled[identifier.key()]; + return !!uninstalled[extensionId.toString()]; } - private async extract({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise { - let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, metadata, token); - this.logService.info('Extracting completed.', identifierWithVersion.id); + private async extract({ zipPath, key, metadata }: InstallableExtension, token: CancellationToken): Promise { + let local = await this.extensionsScanner.extractUserExtension(key, zipPath, metadata, token); + this.logService.info('Extracting completed.', key.id); return local; } @@ -274,12 +249,12 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { constructor( private readonly gallery: IGalleryExtension, - private readonly options: InstallOptions, + options: InstallOptions, private readonly extensionsDownloader: ExtensionsDownloader, extensionsScanner: ExtensionsScanner, logService: ILogService, ) { - super(gallery.identifier, gallery, extensionsScanner, logService); + super(gallery.identifier, gallery, options, extensionsScanner, logService); } protected async doRun(token: CancellationToken): Promise { @@ -292,6 +267,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { const installableExtension = await this.downloadInstallableExtension(this.gallery, this._operation); installableExtension.metadata.isMachineScoped = this.options.isMachineScoped || existingExtension?.isMachineScoped; installableExtension.metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin; + installableExtension.metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined; + installableExtension.metadata.updated = !!existingExtension; installableExtension.metadata.isPreReleaseVersion = this.gallery.properties.isPreReleaseVersion; installableExtension.metadata.preRelease = this.gallery.properties.isPreReleaseVersion || (isBoolean(this.options.installPreReleaseVersion) @@ -300,7 +277,7 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { try { const local = await this.installExtension(installableExtension, token); - if (existingExtension && semver.neq(existingExtension.manifest.version, this.gallery.version)) { + if (existingExtension && (existingExtension.targetPlatform !== local.targetPlatform || semver.neq(existingExtension.manifest.version, local.manifest.version))) { await this.extensionsScanner.setUninstalled(existingExtension); } return local; @@ -324,6 +301,7 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { id: extension.identifier.uuid, publisherId: extension.publisherId, publisherDisplayName: extension.publisherDisplayName, + targetPlatform: extension.properties.targetPlatform }; let zipPath: string | undefined; @@ -336,8 +314,8 @@ class InstallGalleryExtensionTask extends AbstractInstallExtensionTask { } try { - const manifest = await getManifest(zipPath); - return (>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }); + await getManifest(zipPath); + return (>{ zipPath, key: ExtensionKey.create(extension), metadata }); } catch (error) { await this.deleteDownloadedVSIX(zipPath); throw new ExtensionManagementError(joinErrors(error).message, ExtensionManagementErrorCode.Invalid); @@ -350,16 +328,16 @@ class InstallVSIXTask extends AbstractInstallExtensionTask { constructor( private readonly manifest: IExtensionManifest, private readonly location: URI, - private readonly options: InstallOptions, + options: InstallOptions, private readonly galleryService: IExtensionGalleryService, extensionsScanner: ExtensionsScanner, logService: ILogService ) { - super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, extensionsScanner, logService); + super({ id: getGalleryExtensionId(manifest.publisher, manifest.name) }, location, options, extensionsScanner, logService); } protected async doRun(token: CancellationToken): Promise { - const identifierWithVersion = new ExtensionIdentifierWithVersion(this.identifier, this.manifest.version); + const extensionKey = new ExtensionKey(this.identifier, this.manifest.version); const installedExtensions = await this.extensionsScanner.scanExtensions(ExtensionType.User); const existing = installedExtensions.find(i => areSameExtensions(this.identifier, i.identifier)); const metadata = await this.getMetadata(this.identifier.id, this.manifest.version, token); @@ -368,7 +346,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask { if (existing) { this._operation = InstallOperation.Update; - if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { + if (extensionKey.equals(new ExtensionKey(existing.identifier, existing.manifest.version))) { try { await this.extensionsScanner.removeExtension(existing, 'existing'); } catch (e) { @@ -380,7 +358,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask { } else { // Remove the extension with same version if it is already uninstalled. // Installing a VSIX extension shall replace the existing extension always. - const existing = await this.unsetUninstalledAndGetLocal(identifierWithVersion); + const existing = await this.unsetUninstalledAndGetLocal(extensionKey); if (existing) { try { await this.extensionsScanner.removeExtension(existing, 'existing'); @@ -390,7 +368,7 @@ class InstallVSIXTask extends AbstractInstallExtensionTask { } } - return this.installExtension({ zipPath: path.resolve(this.location.fsPath), identifierWithVersion, metadata }, token); + return this.installExtension({ zipPath: path.resolve(this.location.fsPath), key: extensionKey, metadata }, token); } private async getMetadata(id: string, version: string, token: CancellationToken): Promise { @@ -427,8 +405,8 @@ class UninstallExtensionTask extends AbstractExtensionTask implements IUni const toUninstall: ILocalExtension[] = []; const userExtensions = await this.extensionsScanner.scanUserExtensions(false); if (this.options.versionOnly) { - const extensionIdentifierWithVersion = new ExtensionIdentifierWithVersion(this.extension.identifier, this.extension.manifest.version); - toUninstall.push(...userExtensions.filter(u => extensionIdentifierWithVersion.equals(new ExtensionIdentifierWithVersion(u.identifier, u.manifest.version)))); + const extensionKey = ExtensionKey.create(this.extension); + toUninstall.push(...userExtensions.filter(u => extensionKey.equals(ExtensionKey.create(u)))); } else { toUninstall.push(...userExtensions.filter(u => areSameExtensions(u.identifier, this.extension.identifier))); } diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts index 593ae22edfc..6945e43052e 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -12,7 +12,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; -import { basename } from 'vs/base/common/resources'; +import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; @@ -21,35 +21,35 @@ import { extract, ExtractError } from 'vs/base/node/zip'; import { localize } from 'vs/nls'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { ExtensionManagementError, ExtensionManagementErrorCode, Metadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, ExtensionIdentifierWithVersion, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; -import { ExtensionType, IExtensionIdentifier, IExtensionManifest, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtensionIdentifier, ExtensionIdentifier, IExtensionManifest, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; - export type ILocalExtensionManifest = IExtensionManifest & { __metadata?: Metadata }; -type IRelaxedLocalExtension = Omit & { isBuiltin: boolean }; +type IRelaxedLocalExtension = ILocalExtension & { type: ExtensionType; isBuiltin: boolean; targetPlatform: TargetPlatform }; export class ExtensionsScanner extends Disposable { - private readonly systemExtensionsPath: string; - private readonly extensionsPath: string; + private readonly systemExtensionsLocation: URI; + private readonly userExtensionsLocation: URI; private readonly uninstalledPath: string; private readonly uninstalledFileLimiter: Queue; constructor( private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, + private readonly targetPlatform: Promise, @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @IProductService private readonly productService: IProductService, ) { super(); - this.systemExtensionsPath = environmentService.builtinExtensionsPath; - this.extensionsPath = environmentService.extensionsPath; - this.uninstalledPath = path.join(this.extensionsPath, '.obsolete'); + this.systemExtensionsLocation = URI.file(environmentService.builtinExtensionsPath); + this.userExtensionsLocation = URI.file(environmentService.extensionsPath); + this.uninstalledPath = joinPath(this.userExtensionsLocation, '.obsolete').fsPath; this.uninstalledFileLimiter = new Queue(); } @@ -62,16 +62,18 @@ export class ExtensionsScanner extends Disposable { const promises: Promise[] = []; if (type === null || type === ExtensionType.System) { - promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal)))); - } - - if (type === null || type === ExtensionType.User) { - promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Internal)))); + promises.push(this.scanDefaultSystemExtensions()); + promises.push(this.environmentService.isBuilt ? Promise.resolve([]) : this.scanDevSystemExtensions()); + } else { + promises.push(Promise.resolve([])); + promises.push(Promise.resolve([])); } + promises.push(this.scanUserExtensions(false)); try { - const result = await Promise.all(promises); - return flatten(result); + const [defaultSystemExtensions, devSystemExtensions, userExtensions] = await Promise.all(promises); + const result = this.dedupExtensions([...defaultSystemExtensions, ...devSystemExtensions, ...userExtensions], await this.targetPlatform); + return type !== null ? result.filter(r => r.type === type) : result; } catch (error) { throw this.joinErrors(error); } @@ -79,35 +81,25 @@ export class ExtensionsScanner extends Disposable { async scanUserExtensions(excludeOutdated: boolean): Promise { this.logService.trace('Started scanning user extensions'); - let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions()]); - extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); - if (excludeOutdated) { - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]); - } + let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanFromUserExtensionsLocation()]); + extensions = extensions.filter(e => !uninstalled[ExtensionKey.create(e).toString()]); + extensions = excludeOutdated ? this.dedupExtensions(extensions, await this.targetPlatform) : extensions; this.logService.trace('Scanned user extensions:', extensions.length); return extensions; } - async scanAllUserExtensions(): Promise { - return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User); - } - - async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise { - const folderName = identifierWithVersion.key(); - const tempPath = path.join(this.extensionsPath, `.${generateUuid()}`); - const extensionPath = path.join(this.extensionsPath, folderName); + async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise { + const folderName = extensionKey.toString(); + const tempPath = path.join(this.userExtensionsLocation.fsPath, `.${generateUuid()}`); + const extensionPath = path.join(this.userExtensionsLocation.fsPath, folderName); try { await pfs.Promises.rm(extensionPath); } catch (error) { - try { - await pfs.Promises.rm(extensionPath); - } catch (e) { /* ignore */ } - throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifierWithVersion.id), ExtensionManagementErrorCode.Delete); + throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete); } - await this.extractAtLocation(identifierWithVersion, zipPath, tempPath, token); + await this.extractAtLocation(extensionKey, zipPath, tempPath, token); let local = await this.scanExtension(URI.file(tempPath), ExtensionType.User); if (!local) { throw new Error(localize('cannot read', "Cannot read the extension from {0}", tempPath)); @@ -115,14 +107,14 @@ export class ExtensionsScanner extends Disposable { await this.storeMetadata(local, { ...metadata, installedTimestamp: Date.now() }); try { - await this.rename(identifierWithVersion, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); + await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); this.logService.info('Renamed to', extensionPath); } catch (error) { try { await pfs.Promises.rm(tempPath); } catch (e) { /* ignore */ } if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, identifierWithVersion.id); + this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); } else { this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath); throw error; @@ -136,7 +128,7 @@ export class ExtensionsScanner extends Disposable { if (local) { return local; } - throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath)); + throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.userExtensionsLocation.fsPath)); } async saveMetadataForLocalExtension(local: ILocalExtension, metadata: Metadata): Promise { @@ -145,6 +137,39 @@ export class ExtensionsScanner extends Disposable { return local; } + getUninstalledExtensions(): Promise> { + return this.withUninstalledExtensions(); + } + + async setUninstalled(...extensions: ILocalExtension[]): Promise { + const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); + await this.withUninstalledExtensions(uninstalled => { + extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); + }); + } + + async setInstalled(extensionKey: ExtensionKey): Promise { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + const userExtensions = await this.scanUserExtensions(true); + const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; + if (!localExtension) { + return null; + } + await this.storeMetadata(localExtension, { installedTimestamp: Date.now() }); + return this.scanExtension(localExtension.location, localExtension.type); + } + + async removeExtension(extension: ILocalExtension, type: string): Promise { + this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); + await pfs.Promises.rm(extension.location.fsPath); + this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); + } + + async removeUninstalledExtension(extension: ILocalExtension): Promise { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); + } + private async storeMetadata(local: ILocalExtension, metaData: Metadata): Promise { // unset if false metaData.isMachineScoped = metaData.isMachineScoped || undefined; @@ -158,28 +183,6 @@ export class ExtensionsScanner extends Disposable { return local; } - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); - } - - async setUninstalled(...extensions: ILocalExtension[]): Promise { - const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version)); - await this.withUninstalledExtensions(uninstalled => { - ids.forEach(id => uninstalled[id.key()] = true); - }); - } - - async setInstalled(identifierWithVersion: ExtensionIdentifierWithVersion): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[identifierWithVersion.key()]); - const installed = await this.scanExtensions(ExtensionType.User); - const localExtension = installed.find(i => new ExtensionIdentifierWithVersion(i.identifier, i.manifest.version).equals(identifierWithVersion)) || null; - if (!localExtension) { - return null; - } - await this.storeMetadata(localExtension, { installedTimestamp: Date.now() }); - return this.scanExtension(localExtension.location, ExtensionType.User); - } - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { return this.uninstalledFileLimiter.queue(async () => { let raw: string | undefined; @@ -211,17 +214,6 @@ export class ExtensionsScanner extends Disposable { }); } - async removeExtension(extension: ILocalExtension, type: string): Promise { - this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - await pfs.Promises.rm(extension.location.fsPath); - this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); - } - - async removeUninstalledExtension(extension: ILocalExtension): Promise { - await this.removeExtension(extension, 'uninstalled'); - await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]); - } - private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); @@ -261,36 +253,23 @@ export class ExtensionsScanner extends Disposable { } } - private async scanSystemExtensions(): Promise { - this.logService.trace('Started scanning system extensions'); - const systemExtensionsPromise = this.scanDefaultSystemExtensions(); - if (this.environmentService.isBuilt) { - return systemExtensionsPromise; - } - - // Scan other system extensions during development - const devSystemExtensionsPromise = this.scanDevSystemExtensions(); - const [systemExtensions, devSystemExtensions] = await Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]); - return [...systemExtensions, ...devSystemExtensions]; - } - - private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { + private async scanExtensionsInLocation(location: URI, preferredType: ExtensionType): Promise { const limiter = new Limiter(10); - const stat = await this.fileService.resolve(URI.file(dir)); + const stat = await this.fileService.resolve(location); if (stat.children) { const extensions = await Promise.all(stat.children.filter(c => c.isDirectory) .map(c => limiter.queue(async () => { - if (type === ExtensionType.User && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.` + if (isEqualOrParent(c.resource, this.userExtensionsLocation) && basename(c.resource).indexOf('.') === 0) { // Do not consider user extension folder starting with `.` return null; } - return this.scanExtension(c.resource, type); + return this.scanExtension(c.resource, preferredType); }))); return extensions.filter(e => e && e.identifier); } return []; } - private async scanExtension(extensionLocation: URI, type: ExtensionType): Promise { + private async scanExtension(extensionLocation: URI, preferredType: ExtensionType): Promise { try { const stat = await this.fileService.resolve(extensionLocation); if (stat.children) { @@ -298,12 +277,13 @@ export class ExtensionsScanner extends Disposable { const readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; const changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const type = metadata?.isSystem ? ExtensionType.System : preferredType; const local = { type, identifier, manifest, location: extensionLocation, readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false, isBuiltin: type === ExtensionType.System }; this.setMetadata(local, metadata); return local; } } catch (e) { - if (type !== ExtensionType.System) { + if (preferredType !== ExtensionType.System) { this.logService.trace(e); } } @@ -311,15 +291,17 @@ export class ExtensionsScanner extends Disposable { } private async scanDefaultSystemExtensions(): Promise { - const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System); + this.logService.trace('Started scanning system extensions'); + const result = await this.scanExtensionsInLocation(this.systemExtensionsLocation, ExtensionType.System); this.logService.trace('Scanned system extensions:', result.length); return result; } private async scanDevSystemExtensions(): Promise { + this.logService.trace('Started scanning dev system extensions'); const devSystemExtensionsList = this.getDevSystemExtensionsList(); if (devSystemExtensionsList.length) { - const result = await this.scanExtensionsInDir(this.devSystemExtensionsPath, ExtensionType.System); + const result = await this.scanExtensionsInLocation(this.devSystemExtensionsLocation, ExtensionType.System); this.logService.trace('Scanned dev system extensions:', result.length); return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id }))); } else { @@ -327,6 +309,35 @@ export class ExtensionsScanner extends Disposable { } } + private async scanFromUserExtensionsLocation(): Promise { + return this.scanExtensionsInLocation(this.userExtensionsLocation, ExtensionType.User); + } + + private dedupExtensions(extensions: ILocalExtension[], targetPlatform: TargetPlatform): ILocalExtension[] { + const result = new Map(); + for (const extension of extensions) { + const extensionKey = ExtensionIdentifier.toKey(extension.identifier.id); + const existing = result.get(extensionKey); + if (existing) { + if (semver.gt(existing.manifest.version, extension.manifest.version)) { + this.logService.debug(`Skipping extension ${extension.location.fsPath} with lower version ${extension.manifest.version}.`); + continue; + } + if (semver.eq(existing.manifest.version, extension.manifest.version) && existing.targetPlatform === targetPlatform) { + this.logService.debug(`Skipping extension ${extension.location.fsPath} from different target platform ${extension.targetPlatform}`); + continue; + } + if (existing.type === ExtensionType.System) { + this.logService.debug(`Overwriting system extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); + } else { + this.logService.warn(`Overwriting user extension ${existing.location.fsPath} with ${extension.location.fsPath}.`); + } + } + result.set(extensionKey, extension); + } + return [...result.values()]; + } + private setMetadata(local: IRelaxedLocalExtension, metadata: Metadata | null): void { local.publisherDisplayName = metadata?.publisherDisplayName || null; local.publisherId = metadata?.publisherId || null; @@ -336,14 +347,16 @@ export class ExtensionsScanner extends Disposable { local.preRelease = !!metadata?.preRelease; local.isBuiltin = local.type === ExtensionType.System || !!metadata?.isBuiltin; local.installedTimestamp = metadata?.installedTimestamp; + local.targetPlatform = metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; + local.updated = !!metadata?.updated; } private async removeUninstalledExtensions(): Promise { const uninstalled = await this.getUninstalledExtensions(); - const extensions = await this.scanAllUserExtensions(); // All user extensions + const extensions = await this.scanFromUserExtensionsLocation(); // All user extensions const installed: Set = new Set(); for (const e of extensions) { - if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) { + if (!uninstalled[ExtensionKey.create(e).toString()]) { installed.add(e.identifier.id.toLowerCase()); } } @@ -354,17 +367,27 @@ export class ExtensionsScanner extends Disposable { await this.beforeRemovingExtension(latest); } })); - const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); + const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); } private async removeOutdatedExtensions(): Promise { - const extensions = await this.scanAllUserExtensions(); + const extensions = await this.scanFromUserExtensionsLocation(); const toRemove: ILocalExtension[] = []; // Outdated extensions + const targetPlatform = await this.targetPlatform; const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))); + toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => { + const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); + if (vcompare !== 0) { + return vcompare; + } + if (a.targetPlatform === targetPlatform) { + return -1; + } + return 1; + }).slice(1)))); await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); } @@ -383,12 +406,12 @@ export class ExtensionsScanner extends Disposable { }, new Error('')); } - private _devSystemExtensionsPath: string | null = null; - private get devSystemExtensionsPath(): string { - if (!this._devSystemExtensionsPath) { - this._devSystemExtensionsPath = path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions')); + private _devSystemExtensionsLocation: URI | null = null; + private get devSystemExtensionsLocation(): URI { + if (!this._devSystemExtensionsLocation) { + this._devSystemExtensionsLocation = URI.file(path.normalize(path.join(FileAccess.asFileUri('', require).fsPath, '..', '.build', 'builtInExtensions'))); } - return this._devSystemExtensionsPath; + return this._devSystemExtensionsLocation; } private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: Metadata | null }> { diff --git a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts index 0c32205fe91..36c669a4951 100644 --- a/src/vs/platform/extensionManagement/node/extensionsWatcher.ts +++ b/src/vs/platform/extensionManagement/node/extensionsWatcher.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { DidUninstallExtensionEvent, IExtensionManagementService, ILocalExtension, InstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { FileChangeType, IFileChange, IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -31,7 +31,7 @@ export class ExtensionsWatcher extends Disposable { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, ) { super(); - this.extensionsManagementService.getInstalled(ExtensionType.User).then(extensions => { + this.extensionsManagementService.getInstalled().then(extensions => { this.installedExtensions = extensions.map(e => e.identifier); this.startTimestamp = Date.now(); }); @@ -111,7 +111,7 @@ export class ExtensionsWatcher extends Disposable { private async onDidChange(): Promise { if (this.installedExtensions) { - const extensions = await this.extensionsManagementService.getInstalled(ExtensionType.User); + const extensions = await this.extensionsManagementService.getInstalled(); const added = extensions.filter(e => { if ([...this.installingExtensions, ...this.installedExtensions!].some(identifier => areSameExtensions(identifier, e.identifier))) { return false; diff --git a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts index 11533d2f326..4312de805f8 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionGalleryService.test.ts @@ -13,7 +13,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IRawGalleryExtensionVersion, sortExtensionVersions } from 'vs/platform/extensionManagement/common/extensionGalleryService'; -import { TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IFileService } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; @@ -23,6 +22,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; import { InMemoryStorageService, IStorageService } from 'vs/platform/storage/common/storage'; import { TelemetryConfiguration, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; class EnvironmentServiceMock extends mock() { override readonly serviceMachineIdResource: URI; diff --git a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts index 82ae6dd9031..9f29115a827 100644 --- a/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts +++ b/src/vs/platform/extensionManagement/test/common/extensionManagement.test.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { TargetPlatform } from 'vs/platform/extensions/common/extensions'; suite('Extension Identifier Pattern', () => { @@ -26,4 +28,17 @@ suite('Extension Identifier Pattern', () => { assert.strictEqual(false, regEx.test('publ_isher.name')); assert.strictEqual(false, regEx.test('publisher._name')); }); + + test('extension key', () => { + assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1').toString(), 'pub.extension-name-1.0.1'); + assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.UNDEFINED).toString(), 'pub.extension-name-1.0.1'); + assert.strictEqual(new ExtensionKey({ id: 'pub.extension-name' }, '1.0.1', TargetPlatform.WIN32_IA32).toString(), `pub.extension-name-1.0.1-${TargetPlatform.WIN32_IA32}`); + }); + + test('extension key parsing', () => { + assert.strictEqual(ExtensionKey.parse('pub.extension-name'), null); + assert.strictEqual(ExtensionKey.parse('pub.extension-name@1.2.3'), null); + assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1')?.toString(), 'pub.extension-name-1.0.1'); + assert.strictEqual(ExtensionKey.parse('pub.extension-name-1.0.1-win32-ia32')?.toString(), 'pub.extension-name-1.0.1-win32-ia32'); + }); }); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 5aa0a9b745e..0bfebc689dd 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -257,42 +257,67 @@ export const EXTENSION_CATEGORIES = [ 'Other', ]; -export interface IExtensionManifest { - readonly name: string; - readonly displayName?: string; - readonly publisher: string; - readonly version: string; - readonly engines: { readonly vscode: string }; - readonly description?: string; - readonly main?: string; - readonly browser?: string; - readonly icon?: string; - readonly categories?: string[]; - readonly keywords?: string[]; - readonly activationEvents?: string[]; - readonly extensionDependencies?: string[]; - readonly extensionPack?: string[]; - readonly extensionKind?: ExtensionKind | ExtensionKind[]; - readonly contributes?: IExtensionContributions; - readonly repository?: { url: string }; - readonly bugs?: { url: string }; - readonly enabledApiProposals?: readonly string[]; - readonly api?: string; - readonly scripts?: { [key: string]: string }; - readonly capabilities?: IExtensionCapabilities; +export interface IRelaxedExtensionManifest { + name: string; + displayName?: string; + publisher: string; + version: string; + engines: { readonly vscode: string }; + description?: string; + main?: string; + browser?: string; + icon?: string; + categories?: string[]; + keywords?: string[]; + activationEvents?: string[]; + extensionDependencies?: string[]; + extensionPack?: string[]; + extensionKind?: ExtensionKind | ExtensionKind[]; + contributes?: IExtensionContributions; + repository?: { url: string }; + bugs?: { url: string }; + enabledApiProposals?: readonly string[]; + api?: string; + scripts?: { [key: string]: string }; + capabilities?: IExtensionCapabilities; } +export type IExtensionManifest = Readonly; + export const enum ExtensionType { System, User } +export const enum TargetPlatform { + WIN32_X64 = 'win32-x64', + WIN32_IA32 = 'win32-ia32', + WIN32_ARM64 = 'win32-arm64', + + LINUX_X64 = 'linux-x64', + LINUX_ARM64 = 'linux-arm64', + LINUX_ARMHF = 'linux-armhf', + + ALPINE_X64 = 'alpine-x64', + ALPINE_ARM64 = 'alpine-arm64', + + DARWIN_X64 = 'darwin-x64', + DARWIN_ARM64 = 'darwin-arm64', + + WEB = 'web', + + UNIVERSAL = 'universal', + UNKNOWN = 'unknown', + UNDEFINED = 'undefined', +} + export interface IExtension { readonly type: ExtensionType; readonly isBuiltin: boolean; readonly identifier: IExtensionIdentifier; readonly manifest: IExtensionManifest; readonly location: URI; + readonly targetPlatform: TargetPlatform; readonly readmeUrl?: URI; readonly changelogUrl?: URI; } @@ -352,15 +377,19 @@ export class ExtensionIdentifier { } } -export interface IExtensionDescription extends IExtensionManifest { - readonly identifier: ExtensionIdentifier; - readonly uuid?: string; - readonly isBuiltin: boolean; - readonly isUserBuiltin: boolean; - readonly isUnderDevelopment: boolean; - readonly extensionLocation: URI; +export interface IRelaxedExtensionDescription extends IRelaxedExtensionManifest { + id?: string; + identifier: ExtensionIdentifier; + uuid?: string; + targetPlatform: TargetPlatform; + isBuiltin: boolean; + isUserBuiltin: boolean; + isUnderDevelopment: boolean; + extensionLocation: URI; } +export type IExtensionDescription = Readonly; + export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; } diff --git a/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts b/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts index 41a96448f98..f5f7003b36e 100644 --- a/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts +++ b/src/vs/platform/externalTerminal/electron-main/externalTerminalService.test.ts @@ -7,7 +7,7 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { DEFAULT_TERMINAL_OSX, IExternalTerminalConfiguration } from 'vs/platform/externalTerminal/common/externalTerminal'; import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from 'vs/platform/externalTerminal/node/externalTerminalService'; -const mockConfig: IExternalTerminalConfiguration = Object.freeze({ +const mockConfig = Object.freeze({ terminal: { explorerKind: 'external', external: { diff --git a/src/vs/platform/files/browser/htmlFileSystemProvider.ts b/src/vs/platform/files/browser/htmlFileSystemProvider.ts index 6912991be0a..bdde288717f 100644 --- a/src/vs/platform/files/browser/htmlFileSystemProvider.ts +++ b/src/vs/platform/files/browser/htmlFileSystemProvider.ts @@ -10,7 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { normalize } from 'vs/base/common/path'; +import { basename, extname, normalize } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; import { extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream'; @@ -144,7 +144,8 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr // Entire file else { - const reader: ReadableStreamDefaultReader = file.stream().getReader(); + // TODO@electron: duplicate type definitions originate from `@types/node/stream/consumers.d.ts` + const reader: ReadableStreamDefaultReader = (file.stream() as unknown as ReadableStream).getReader(); let res = await reader.read(); while (!res.done) { @@ -314,9 +315,12 @@ export class HTMLFileSystemProvider implements IFileSystemProviderWithFileReadWr // Compute a valid handle ID in case this exists already if (map.has(handleId) && !await map.get(handleId)?.isSameEntry(handle)) { - let handleIdCounter = 2; + const fileExt = extname(handle.name); + const fileName = basename(handle.name, fileExt); + + let handleIdCounter = 1; do { - handleId = `/${handle.name}-${handleIdCounter++}`; + handleId = `/${fileName}-${handleIdCounter++}${fileExt}`; } while (map.has(handleId) && !await map.get(handleId)?.isSameEntry(handle)); } diff --git a/src/vs/platform/files/common/diskFileSystemProviderClient.ts b/src/vs/platform/files/common/diskFileSystemProviderClient.ts index 1980081887f..f797b86ca94 100644 --- a/src/vs/platform/files/common/diskFileSystemProviderClient.ts +++ b/src/vs/platform/files/common/diskFileSystemProviderClient.ts @@ -13,7 +13,7 @@ import { newWriteableStream, ReadableStreamEventPayload, ReadableStreamEvents } import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { createFileSystemProviderError, FileAtomicReadOptions, FileChangeType, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { createFileSystemProviderError, FileAtomicReadOptions, FileChangeType, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileChange, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; export const LOCAL_FILE_SYSTEM_CHANNEL_NAME = 'localFilesystem'; @@ -27,7 +27,8 @@ export class DiskFileSystemProviderClient extends Disposable implements IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, - IFileSystemProviderWithFileAtomicReadCapability { + IFileSystemProviderWithFileAtomicReadCapability, + IFileSystemProviderWithFileCloneCapability { constructor( private readonly channel: IChannel, @@ -51,7 +52,8 @@ export class DiskFileSystemProviderClient extends Disposable implements FileSystemProviderCapabilities.FileReadStream | FileSystemProviderCapabilities.FileFolderCopy | FileSystemProviderCapabilities.FileWriteUnlock | - FileSystemProviderCapabilities.FileAtomicRead; + FileSystemProviderCapabilities.FileAtomicRead | + FileSystemProviderCapabilities.FileClone; if (this.extraCapabilities.pathCaseSensitive) { this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; @@ -189,6 +191,14 @@ export class DiskFileSystemProviderClient extends Disposable implements //#endregion + //#region Clone File + + cloneFile(resource: URI, target: URI): Promise { + return this.channel.call('cloneFile', [resource, target]); + } + + //#endregion + //#region File Watching private readonly _onDidChange = this._register(new Emitter()); diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 8af68ed1d5e..541f72b92f6 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from 'vs/base/c import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from 'vs/base/common/stream'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, FileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, FileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability } from 'vs/platform/files/common/files'; import { readFileIntoStream } from 'vs/platform/files/common/io'; import { ILogService } from 'vs/platform/log/common/log'; @@ -369,9 +369,9 @@ export class FileService extends Disposable implements IFileService { } // optimization: if the provider has unbuffered write capability and the data - // to write is a Readable, we consume up to 3 chunks and try to write the data - // unbuffered to reduce the overhead. If the Readable has more data to provide - // we continue to write buffered. + // to write is not a buffer, we consume up to 3 chunks and try to write the data + // unbuffered to reduce the overhead. If the stream or readable has more data + // to provide we continue to write buffered. let bufferOrReadableOrStreamOrBufferedStream: VSBuffer | VSBufferReadable | VSBufferReadableStream | VSBufferReadableBufferedStream; if (hasReadWriteCapability(provider) && !(bufferOrReadableOrStream instanceof VSBuffer)) { if (isReadableStream(bufferOrReadableOrStream)) { @@ -1002,6 +1002,43 @@ export class FileService extends Disposable implements IFileService { //#endregion + //#region Clone File + + async cloneFile(source: URI, target: URI): Promise { + const sourceProvider = await this.withProvider(source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); + + if (sourceProvider === targetProvider && this.getExtUri(sourceProvider).providerExtUri.isEqual(source, target)) { + return; // return early if paths are equal + } + + // same provider, use `cloneFile` when native support is provided + if (sourceProvider === targetProvider && hasFileCloneCapability(sourceProvider)) { + return sourceProvider.cloneFile(source, target); + } + + // otherwise, either providers are different or there is no native + // `cloneFile` support, then we fallback to emulate a clone as best + // as we can with the other primitives + + // create parent folders + await this.mkdirp(targetProvider, this.getExtUri(targetProvider).providerExtUri.dirname(target)); + + // queue on the source to ensure atomic read + const sourceWriteQueue = this.writeQueue.queueFor(source, this.getExtUri(sourceProvider).providerExtUri); + + // leverage `copy` method if provided and providers are identical + if (sourceProvider === targetProvider && hasFileFolderCopyCapability(sourceProvider)) { + return sourceWriteQueue.queue(() => sourceProvider.copy(source, target, { overwrite: true })); + } + + // otherwise copy via buffer/unbuffered and use a write queue + // on the source to ensure atomic operation as much as possible + return sourceWriteQueue.queue(() => this.doCopyFile(sourceProvider, source, targetProvider, target)); + } + + //#endregion + //#region File Watching private readonly _onDidFilesChange = this._register(new Emitter()); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ab6b0c91625..fad82f592ef 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -97,7 +97,9 @@ export interface IFileService { readonly onDidRunOperation: Event; /** - * Resolve the properties of a file/folder identified by the resource. + * Resolve the properties of a file/folder identified by the resource. For a folder, children + * information is resolved as well depending on the provided options. Use `stat()` method if + * you do not need children information. * * If the optional parameter "resolveTo" is specified in options, the stat service is asked * to provide a stat object that should contain the full graph of folders up to all of the @@ -145,6 +147,8 @@ export interface IFileService { /** * Updates the content replacing its previous value. + * + * Emits a `FileOperation.WRITE` file operation event when successful. */ writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise; @@ -152,6 +156,8 @@ export interface IFileService { * Moves the file/folder to a new path identified by the resource. * * The optional parameter overwrite can be set to replace an existing file at the location. + * + * Emits a `FileOperation.MOVE` file operation event when successful. */ move(source: URI, target: URI, overwrite?: boolean): Promise; @@ -162,9 +168,10 @@ export interface IFileService { canMove(source: URI, target: URI, overwrite?: boolean): Promise; /** - * Copies the file/folder to a path identified by the resource. + * Copies the file/folder to a path identified by the resource. A folder is copied + * recursively. * - * The optional parameter overwrite can be set to replace an existing file at the location. + * Emits a `FileOperation.COPY` file operation event when successful. */ copy(source: URI, target: URI, overwrite?: boolean): Promise; @@ -175,22 +182,33 @@ export interface IFileService { canCopy(source: URI, target: URI, overwrite?: boolean): Promise; /** - * Find out if a file create operation is possible given the arguments. No changes on disk will - * be performed. Returns an Error if the operation cannot be done. + * Clones a file to a path identified by the resource. Folders are not supported. + * + * If the target path exists, it will be overwritten. */ - canCreateFile(resource: URI, options?: ICreateFileOptions): Promise; + cloneFile(source: URI, target: URI): Promise; /** * Creates a new file with the given path and optional contents. The returned promise * will have the stat model object as a result. * * The optional parameter content can be used as value to fill into the new file. + * + * Emits a `FileOperation.CREATE` file operation event when successful. */ createFile(resource: URI, bufferOrReadableOrStream?: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: ICreateFileOptions): Promise; + /** + * Find out if a file create operation is possible given the arguments. No changes on disk will + * be performed. Returns an Error if the operation cannot be done. + */ + canCreateFile(resource: URI, options?: ICreateFileOptions): Promise; + /** * Creates a new folder with the given path. The returned promise * will have the stat model object as a result. + * + * Emits a `FileOperation.CREATE` file operation event when successful. */ createFolder(resource: URI): Promise; @@ -198,6 +216,8 @@ export interface IFileService { * Deletes the provided file. The optional useTrash parameter allows to * move the file to trash. The optional recursive parameter allows to delete * non-empty folders recursively. + * + * Emits a `FileOperation.DELETE` file operation event when successful. */ del(resource: URI, options?: Partial): Promise; @@ -459,7 +479,12 @@ export const enum FileSystemProviderCapabilities { * Provider support to read files atomically. This implies the * provider provides the `FileReadWrite` capability too. */ - FileAtomicRead = 1 << 14 + FileAtomicRead = 1 << 14, + + /** + * Provider support to clone files atomically. + */ + FileClone = 1 << 15 } export interface IFileSystemProvider { @@ -488,6 +513,8 @@ export interface IFileSystemProvider { close?(fd: number): Promise; read?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; write?(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise; + + cloneFile?(from: URI, to: URI): Promise; } export interface IFileSystemProviderWithFileReadWriteCapability extends IFileSystemProvider { @@ -507,6 +534,14 @@ export function hasFileFolderCopyCapability(provider: IFileSystemProvider): prov return !!(provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy); } +export interface IFileSystemProviderWithFileCloneCapability extends IFileSystemProvider { + cloneFile(from: URI, to: URI): Promise; +} + +export function hasFileCloneCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileCloneCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.FileClone); +} + export interface IFileSystemProviderWithOpenReadWriteCloseCapability extends IFileSystemProvider { open(resource: URI, opts: FileOpenOptions): Promise; close(fd: number): Promise; diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 7719618d724..15eecd7e844 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -11,7 +11,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { isEqual } from 'vs/base/common/extpath'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { basename, dirname } from 'vs/base/common/path'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources'; @@ -19,7 +19,7 @@ import { newWriteableStream, ReadableStreamEvents } from 'vs/base/common/stream' import { URI } from 'vs/base/common/uri'; import { IDirent, Promises, RimRafMode, SymlinkSupport } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; -import { createFileSystemProviderError, FileAtomicReadOptions, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat } from 'vs/platform/files/common/files'; +import { createFileSystemProviderError, FileAtomicReadOptions, FileDeleteOptions, FileOpenOptions, FileOverwriteOptions, FileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, FileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat } from 'vs/platform/files/common/files'; import { readFileIntoStream } from 'vs/platform/files/common/io'; import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, IDiskFileChange, ILogMessage } from 'vs/platform/files/common/watcher'; import { ILogService } from 'vs/platform/log/common/log'; @@ -45,7 +45,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, - IFileSystemProviderWithFileAtomicReadCapability { + IFileSystemProviderWithFileAtomicReadCapability, + IFileSystemProviderWithFileCloneCapability { constructor( logService: ILogService, @@ -67,7 +68,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple FileSystemProviderCapabilities.FileReadStream | FileSystemProviderCapabilities.FileFolderCopy | FileSystemProviderCapabilities.FileWriteUnlock | - FileSystemProviderCapabilities.FileAtomicRead; + FileSystemProviderCapabilities.FileAtomicRead | + FileSystemProviderCapabilities.FileClone; if (isLinux) { this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive; @@ -617,6 +619,54 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple //#endregion + //#region Clone File + + async cloneFile(from: URI, to: URI): Promise { + return this.doCloneFile(from, to, false /* optimistically assume parent folders exist */); + } + + private async doCloneFile(from: URI, to: URI, mkdir: boolean): Promise { + const fromFilePath = this.toFilePath(from); + const toFilePath = this.toFilePath(to); + + const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + if (isEqual(fromFilePath, toFilePath, !isPathCaseSensitive)) { + return; // cloning is only supported `from` and `to` are different files + } + + // Implement clone by using `fs.copyFile`, however setup locks + // for both `from` and `to` because node.js does not ensure + // this to be an atomic operation + + const locks = new DisposableStore(); + + try { + const [fromLock, toLock] = await Promise.all([ + this.createResourceLock(from), + this.createResourceLock(to) + ]); + + locks.add(fromLock); + locks.add(toLock); + + if (mkdir) { + await Promises.mkdir(dirname(toFilePath), { recursive: true }); + } + + await Promises.copyFile(fromFilePath, toFilePath); + } catch (error) { + if (error.code === 'ENOENT' && !mkdir) { + return this.doCloneFile(from, to, true); + } + + throw this.toFileSystemProviderError(error); + } finally { + locks.dispose(); + } + } + + //#endregion + //#region File Watching protected createUniversalWatcher( diff --git a/src/vs/platform/files/node/diskFileSystemProviderServer.ts b/src/vs/platform/files/node/diskFileSystemProviderServer.ts index 1b350c9e72a..d51e77b3dce 100644 --- a/src/vs/platform/files/node/diskFileSystemProviderServer.ts +++ b/src/vs/platform/files/node/diskFileSystemProviderServer.ts @@ -47,6 +47,7 @@ export abstract class AbstractDiskFileSystemProviderChannel extends Disposabl case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]); case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]); case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]); + case 'cloneFile': return this.cloneFile(uriTransformer, arg[0], arg[1]); case 'mkdir': return this.mkdir(uriTransformer, arg[0]); case 'delete': return this.delete(uriTransformer, arg[0], arg[1]); case 'watch': return this.watch(uriTransformer, arg[0], arg[1], arg[2], arg[3]); @@ -187,6 +188,17 @@ export abstract class AbstractDiskFileSystemProviderChannel extends Disposabl //#endregion + //#region Clone File + + private cloneFile(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents): Promise { + const source = this.transformIncoming(uriTransformer, _source); + const target = this.transformIncoming(uriTransformer, _target); + + return this.provider.cloneFile(source, target); + } + + //#endregion + //#region File Watching private readonly sessionToWatcher = new Map(); diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts index ec314be1b61..3387e3b989b 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.test.ts @@ -26,7 +26,7 @@ flakySuite('IndexedDBFileSystemProvider', function () { const testDir = '/'; const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); - const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.userData, path: testDir }), ...paths); + const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.vscodeUserData, path: testDir }), ...paths); const disposables = new DisposableStore(); @@ -73,8 +73,8 @@ flakySuite('IndexedDBFileSystemProvider', function () { disposables.add(service.registerProvider(logSchema, logFileProvider)); disposables.add(logFileProvider); - userdataFileProvider = new IndexedDBFileSystemProvider(Schemas.userData, indexedDB, 'vscode-userdata-store', true); - disposables.add(service.registerProvider(Schemas.userData, userdataFileProvider)); + userdataFileProvider = new IndexedDBFileSystemProvider(Schemas.vscodeUserData, indexedDB, 'vscode-userdata-store', true); + disposables.add(service.registerProvider(Schemas.vscodeUserData, userdataFileProvider)); disposables.add(userdataFileProvider); }; diff --git a/src/vs/platform/files/test/node/diskFileService.test.ts b/src/vs/platform/files/test/node/diskFileService.test.ts index 99f0d59c753..09b6205f48d 100644 --- a/src/vs/platform/files/test/node/diskFileService.test.ts +++ b/src/vs/platform/files/test/node/diskFileService.test.ts @@ -67,9 +67,10 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileReadStream | FileSystemProviderCapabilities.Trash | + FileSystemProviderCapabilities.FileFolderCopy | FileSystemProviderCapabilities.FileWriteUnlock | FileSystemProviderCapabilities.FileAtomicRead | - FileSystemProviderCapabilities.FileFolderCopy; + FileSystemProviderCapabilities.FileClone; if (isLinux) { this._testCapabilities |= FileSystemProviderCapabilities.PathCaseSensitive; @@ -1166,6 +1167,67 @@ flakySuite('Disk File Service', function () { assert.strictEqual(source.size, copied.size); }); + test('cloneFile - basics', () => { + return testCloneFile(); + }); + + test('cloneFile - via copy capability', () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileFolderCopy); + + return testCloneFile(); + }); + + test('cloneFile - via pipe', () => { + setCapabilities(fileProvider, FileSystemProviderCapabilities.FileOpenReadWriteClose); + + return testCloneFile(); + }); + + async function testCloneFile(): Promise { + const source1 = URI.file(join(testDir, 'index.html')); + const source1Size = (await service.resolve(source1, { resolveMetadata: true })).size; + + const source2 = URI.file(join(testDir, 'lorem.txt')); + const source2Size = (await service.resolve(source2, { resolveMetadata: true })).size; + + const targetParent = URI.file(testDir); + + // same path is a no-op + await service.cloneFile(source1, source1); + + // simple clone to existing parent folder path + const target1 = targetParent.with({ path: posix.join(targetParent.path, `${posix.basename(source1.path)}-clone`) }); + + await service.cloneFile(source1, URI.file(target1.fsPath)); + + assert.strictEqual(existsSync(target1.fsPath), true); + assert.strictEqual(basename(target1.fsPath), 'index.html-clone'); + + let target1Size = (await service.resolve(target1, { resolveMetadata: true })).size; + + assert.strictEqual(source1Size, target1Size); + + // clone to same path overwrites + await service.cloneFile(source2, URI.file(target1.fsPath)); + + target1Size = (await service.resolve(target1, { resolveMetadata: true })).size; + + assert.strictEqual(source2Size, target1Size); + assert.notStrictEqual(source1Size, target1Size); + + // clone creates missing folders ad-hoc + const target2 = targetParent.with({ path: posix.join(targetParent.path, 'foo', 'bar', `${posix.basename(source1.path)}-clone`) }); + + await service.cloneFile(source1, URI.file(target2.fsPath)); + + assert.strictEqual(existsSync(target2.fsPath), true); + assert.strictEqual(basename(target2.fsPath), 'index.html-clone'); + + let target2Size = (await service.resolve(target2, { resolveMetadata: true })).size; + + assert.strictEqual(source1Size, target2Size); + } + test('readFile - small file - default', () => { return testReadFile(URI.file(join(testDir, 'small.txt'))); }); diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 12f97ec579c..23ec6fdbc1b 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -96,7 +96,7 @@ export class InstantiationService implements IInstantiationService { // check for argument mismatches, adjust static args if needed if (args.length !== firstServiceArgPos) { - console.warn(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); + console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); let delta = firstServiceArgPos - args.length; if (delta > 0) { diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 2e6b1fadec5..828e7ae65b3 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrowserWindow, Display, ipcMain, IpcMainEvent, screen } from 'electron'; +import { BrowserWindow, BrowserWindowConstructorOptions, Display, ipcMain, IpcMainEvent, screen } from 'electron'; import { arch, release, type } from 'os'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -337,10 +337,11 @@ export class IssueMainService implements ICommonIssueService { nativeWindowOpen: true, zoomFactor: zoomLevelToZoomFactor(options.zoomLevel), sandbox: true, - contextIsolation: true, + contextIsolation: true }, - alwaysOnTop: options.alwaysOnTop - }); + alwaysOnTop: options.alwaysOnTop, + experimentalDarkMode: true + } as BrowserWindowConstructorOptions & { experimentalDarkMode: boolean }); window.setMenuBarVisibility(false); diff --git a/src/vs/platform/layout/browser/layoutService.ts b/src/vs/platform/layout/browser/layoutService.ts index 024d0768ad1..6b6575e27b0 100644 --- a/src/vs/platform/layout/browser/layoutService.ts +++ b/src/vs/platform/layout/browser/layoutService.ts @@ -25,6 +25,15 @@ export interface ILayoutService { /** * Container of the application. + * + * **NOTE**: In the standalone editor case, multiple editors can be created on a page. + * Therefore, in the standalone editor case, there are multiple containers, not just + * a single one. If you ship code that needs a "container" for the standalone editor, + * please use `ICodeEditorService` to get the current focused code editor and use its + * container if necessary. You can also instantiate `EditorScopedLayoutService` + * which implements `ILayoutService` but is not a part of the service collection because + * it is code editor instance specific. + * */ readonly container: HTMLElement; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 3bd0c5742aa..02d80831b53 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -31,7 +31,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; import { IProductService } from 'vs/platform/product/common/productService'; import { ISharedProcess } from 'vs/platform/sharedProcess/node/sharedProcess'; -import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { ICodeWindow } from 'vs/platform/window/electron-main/window'; @@ -54,7 +53,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain @IDialogMainService private readonly dialogMainService: IDialogMainService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, - @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @IProductService private readonly productService: IProductService, @IThemeMainService private readonly themeMainService: IThemeMainService, @@ -391,7 +389,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async pickFileFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise { const paths = await this.dialogMainService.pickFileFolder(options); if (paths) { - this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFileFolder', options.telemetryExtraData); this.doOpenPicked(await Promise.all(paths.map(async path => (await SymlinkSupport.existsDirectory(path)) ? { folderUri: URI.file(path) } : { fileUri: URI.file(path) })), options, windowId); } } @@ -399,7 +396,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async pickFolderAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise { const paths = await this.dialogMainService.pickFolder(options); if (paths) { - this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFolder', options.telemetryExtraData); this.doOpenPicked(paths.map(path => ({ folderUri: URI.file(path) })), options, windowId); } } @@ -407,7 +403,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async pickFileAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise { const paths = await this.dialogMainService.pickFile(options); if (paths) { - this.sendPickerTelemetry(paths, options.telemetryEventName || 'openFile', options.telemetryExtraData); this.doOpenPicked(paths.map(path => ({ fileUri: URI.file(path) })), options, windowId); } } @@ -415,7 +410,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async pickWorkspaceAndOpen(windowId: number | undefined, options: INativeOpenDialogOptions): Promise { const paths = await this.dialogMainService.pickWorkspace(options); if (paths) { - this.sendPickerTelemetry(paths, options.telemetryEventName || 'openWorkspace', options.telemetryExtraData); this.doOpenPicked(paths.map(path => ({ workspaceUri: URI.file(path) })), options, windowId); } } @@ -431,18 +425,6 @@ export class NativeHostMainService extends Disposable implements INativeHostMain }); } - private sendPickerTelemetry(paths: string[], telemetryEventName: string, telemetryExtraData?: ITelemetryData) { - const numberOfPaths = paths ? paths.length : 0; - - // Telemetry - // __GDPR__TODO__ Dynamic event names and dynamic properties. Can not be registered statically. - this.telemetryService.publicLog(telemetryEventName, { - ...telemetryExtraData, - outcome: numberOfPaths ? 'success' : 'canceled', - numberOfPaths - }); - } - //#endregion diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index e22b33e6dcb..5f8c4bcc101 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -58,7 +58,7 @@ else { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.65.0-dev', + version: '1.66.0-dev', nameShort: 'Code - OSS Dev', nameLong: 'Code - OSS Dev', applicationName: 'code-oss', @@ -66,13 +66,7 @@ else { urlProtocol: 'code-oss', reportIssueUrl: 'https://github.com/microsoft/vscode/issues/new', licenseName: 'MIT', - licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt', - extensionAllowedProposedApi: [ - 'ms-vscode.vscode-js-profile-flame', - 'ms-vscode.vscode-js-profile-table', - 'GitHub.remotehub', - 'GitHub.remotehub-insiders' - ], + licenseUrl: 'https://github.com/microsoft/vscode/blob/main/LICENSE.txt' }); } } diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 17b66a65362..3b00369640a 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -94,7 +94,7 @@ export interface IProgressRunner { done(): void; } -export const emptyProgressRunner: IProgressRunner = Object.freeze({ +export const emptyProgressRunner = Object.freeze({ total() { }, worked() { }, done() { } @@ -106,7 +106,7 @@ export interface IProgress { export class Progress implements IProgress { - static readonly None: IProgress = Object.freeze({ report() { } }); + static readonly None = Object.freeze>({ report() { } }); private _value?: T; get value(): T | undefined { return this._value; } diff --git a/src/vs/platform/protocol/electron-main/protocolMainService.ts b/src/vs/platform/protocol/electron-main/protocolMainService.ts index 05e18924db4..324c8abbece 100644 --- a/src/vs/platform/protocol/electron-main/protocolMainService.ts +++ b/src/vs/platform/protocol/electron-main/protocolMainService.ts @@ -22,7 +22,7 @@ export class ProtocolMainService extends Disposable implements IProtocolMainServ declare readonly _serviceBrand: undefined; private readonly validRoots = TernarySearchTree.forPaths(!isLinux); - private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp']); // https://github.com/microsoft/vscode/issues/119384 + private readonly validExtensions = new Set(['.svg', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']); // https://github.com/microsoft/vscode/issues/119384 constructor( @INativeEnvironmentService environmentService: INativeEnvironmentService, diff --git a/src/vs/platform/remote/common/remoteAgentEnvironment.ts b/src/vs/platform/remote/common/remoteAgentEnvironment.ts index 3feda96194f..c1d3bfb2357 100644 --- a/src/vs/platform/remote/common/remoteAgentEnvironment.ts +++ b/src/vs/platform/remote/common/remoteAgentEnvironment.ts @@ -17,6 +17,7 @@ export interface IRemoteAgentEnvironment { extensionHostLogsPath: URI; globalStorageHome: URI; workspaceStorageHome: URI; + localHistoryHome: URI; userHome: URI; os: OperatingSystem; arch: string; diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index f394d3bbbec..663eeaee64b 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -61,7 +62,7 @@ export enum RemoteAuthorityResolverErrorCode { NoResolverFound = 'NoResolverFound' } -export class RemoteAuthorityResolverError extends Error { +export class RemoteAuthorityResolverError extends ErrorNoTelemetry { public static isTemporarilyNotAvailable(err: any): boolean { return (err instanceof RemoteAuthorityResolverError) && err._code === RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable; diff --git a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts index 387e9c3ee69..13353935b7d 100644 --- a/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts +++ b/src/vs/platform/remote/electron-sandbox/remoteAuthorityResolverService.ts @@ -113,7 +113,8 @@ export class RemoteAuthorityResolverService extends Disposable implements IRemot _setResolvedAuthorityError(authority: string, err: any): void { if (this._resolveAuthorityRequests.has(authority)) { const request = this._resolveAuthorityRequests.get(authority)!; - request.reject(err); + // Avoid that this error makes it to telemetry + request.reject(errors.ErrorNoTelemetry.fromError(err)); } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index f9780c42410..3196fe1fcfc 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -16,7 +16,7 @@ import { isBoolean, isNumber } from 'vs/base/common/types'; import { IRequestContext, IRequestOptions } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; -import { getResolvedShellEnv } from 'vs/platform/terminal/node/shellEnv'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService } from 'vs/platform/log/common/log'; import { IHTTPConfiguration, IRequestService } from 'vs/platform/request/common/request'; import { Agent, getProxyAgent } from 'vs/platform/request/node/proxy'; diff --git a/src/vs/platform/sharedProcess/node/sharedProcessEnvironmentService.ts b/src/vs/platform/sharedProcess/node/sharedProcessEnvironmentService.ts index ecf3f1d6aac..7cb9f0d402f 100644 --- a/src/vs/platform/sharedProcess/node/sharedProcessEnvironmentService.ts +++ b/src/vs/platform/sharedProcess/node/sharedProcessEnvironmentService.ts @@ -11,6 +11,6 @@ import { NativeEnvironmentService } from 'vs/platform/environment/node/environme export class SharedProcessEnvironmentService extends NativeEnvironmentService { @memoize - override get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); } + override get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } } diff --git a/src/vs/platform/terminal/node/shellEnv.ts b/src/vs/platform/shell/node/shellEnv.ts similarity index 100% rename from src/vs/platform/terminal/node/shellEnv.ts rename to src/vs/platform/shell/node/shellEnv.ts diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index b817dd78327..59a30ec6934 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -330,3 +330,10 @@ export class WorkspaceStorageMain extends BaseStorageMain implements IStorageMai } } } + +export class InMemoryStorageMain extends BaseStorageMain { + + protected async doCreate(): Promise { + return new Storage(new InMemoryStorageDatabase()); + } +} diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 36658666615..c398abc7927 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -9,10 +9,10 @@ import { IStorage } from 'vs/base/parts/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; +import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { AbstractStorageService, IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { GlobalStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; +import { GlobalStorageMain, InMemoryStorageMain, IStorageMain, IStorageMainOptions, WorkspaceStorageMain } from 'vs/platform/storage/electron-main/storageMain'; import { IAnyWorkspaceIdentifier, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; //#region Storage Main Service (intent: make global and workspace storage accessible to windows from main process) @@ -44,6 +44,8 @@ export class StorageMainService extends Disposable implements IStorageMainServic declare readonly _serviceBrand: undefined; + private shutdownReason: ShutdownReason | undefined = undefined; + constructor( @ILogService private readonly logService: ILogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @@ -80,6 +82,9 @@ export class StorageMainService extends Disposable implements IStorageMainServic this._register(this.lifecycleMainService.onWillShutdown(e => { this.logService.trace('storageMainService#onWillShutdown()'); + // Remember shutdown reason + this.shutdownReason = e.reason; + // Global Storage e.join(this.globalStorage.close()); @@ -132,9 +137,14 @@ export class StorageMainService extends Disposable implements IStorageMainServic } private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { - const workspaceStorage = new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService); + if (this.shutdownReason === ShutdownReason.KILL) { + // Workaround for native crashes that we see when + // SQLite DBs are being created even after shutdown + // https://github.com/microsoft/vscode/issues/143186 + return new InMemoryStorageMain(this.logService); + } - return workspaceStorage; + return new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService); } //#endregion diff --git a/src/vs/platform/storage/test/browser/storageService.test.ts b/src/vs/platform/storage/test/browser/storageService.test.ts index 2b1ed5b22e6..cf51c013ef8 100644 --- a/src/vs/platform/storage/test/browser/storageService.test.ts +++ b/src/vs/platform/storage/test/browser/storageService.test.ts @@ -23,7 +23,7 @@ async function createStorageService(): Promise<[DisposableStore, BrowserStorageS const fileService = disposables.add(new FileService(logService)); const userDataProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider(Schemas.userData, userDataProvider)); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, userDataProvider)); const storageService = disposables.add(new BrowserStorageService({ id: 'workspace-storage-test' }, logService)); diff --git a/src/vs/platform/telemetry/common/gdprTypings.ts b/src/vs/platform/telemetry/common/gdprTypings.ts index d9b9badd404..316c84fca8d 100644 --- a/src/vs/platform/telemetry/common/gdprTypings.ts +++ b/src/vs/platform/telemetry/common/gdprTypings.ts @@ -5,6 +5,8 @@ export interface IPropertyData { classification: 'SystemMetaData' | 'CallstackOrException' | 'CustomerContent' | 'PublicNonPersonalData' | 'EndUserPseudonymizedInformation'; purpose: 'PerformanceAndHealth' | 'FeatureInsight' | 'BusinessInsight'; + owner?: string; + comment?: string; expiration?: string; endpoint?: string; isMeasurement?: boolean; diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index 4a0e58636a5..48dfb1cc8fe 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -5,6 +5,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IProductService } from 'vs/platform/product/common/productService'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryData, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; @@ -30,9 +31,10 @@ export class ServerTelemetryService extends TelemetryService implements IServerT constructor( config: ITelemetryServiceConfig, injectedTelemetryLevel: TelemetryLevel | undefined, - @IConfigurationService _configurationService: IConfigurationService + @IConfigurationService _configurationService: IConfigurationService, + @IProductService _productService: IProductService ) { - super(config, _configurationService); + super(config, _configurationService, _productService); this._injectedTelemetryLevel = injectedTelemetryLevel; } diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 8b4d12a49ba..751bd97d6f9 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -11,6 +11,7 @@ import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryData, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; @@ -35,20 +36,21 @@ export class TelemetryService implements ITelemetryService { private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; private _telemetryLevel: TelemetryLevel; - public readonly sendErrorTelemetry: boolean; + private _sendErrorTelemetry: boolean; private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; constructor( config: ITelemetryServiceConfig, - @IConfigurationService private _configurationService: IConfigurationService + @IConfigurationService private _configurationService: IConfigurationService, + @IProductService private _productService: IProductService ) { this._appenders = config.appenders; this._commonProperties = config.commonProperties || Promise.resolve({}); this._piiPaths = config.piiPaths || []; this._telemetryLevel = TelemetryLevel.USAGE; - this.sendErrorTelemetry = !!config.sendErrorTelemetry; + this._sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi]; @@ -68,12 +70,24 @@ export class TelemetryService implements ITelemetryService { private _updateTelemetryLevel(): void { this._telemetryLevel = getTelemetryLevel(this._configurationService); + const collectableTelemetry = this._productService.enabledTelemetryLevels; + // Also ensure that error telemetry is respecting the product configuration for collectable telemetry + if (collectableTelemetry) { + this._sendErrorTelemetry = this.sendErrorTelemetry ? collectableTelemetry.error : false; + // Make sure the telemetry level from the service is the minimum of the config and product + const maxCollectableTelemetryLevel = collectableTelemetry.usage ? TelemetryLevel.USAGE : collectableTelemetry.error ? TelemetryLevel.ERROR : TelemetryLevel.NONE; + this._telemetryLevel = Math.min(this._telemetryLevel, maxCollectableTelemetryLevel); + } } get telemetryLevel(): TelemetryLevel { return this._telemetryLevel; } + get sendErrorTelemetry(): boolean { + return this._sendErrorTelemetry; + } + async getTelemetryInfo(): Promise { const values = await this._commonProperties; @@ -130,7 +144,7 @@ export class TelemetryService implements ITelemetryService { } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { - if (!this.sendErrorTelemetry) { + if (!this._sendErrorTelemetry) { return Promise.resolve(undefined); } @@ -225,7 +239,7 @@ export class TelemetryService implements ITelemetryService { } function getTelemetryLevelSettingDescription(): string { - const telemetryText = localize('telemetry.telemetryLevelMd', "Controls all core and first party extension telemetry. This helps us to better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong); + const telemetryText = localize('telemetry.telemetryLevelMd', "Controls {0} telemetry, first-party extension and participating third-party extension telemetry. Some third party extensions might not respect this setting, consult the specific extension's documentation to be sure. This helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong); const externalLinksStatement = !product.privacyStatementUrl ? localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') : localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl); diff --git a/src/vs/platform/telemetry/node/appInsightsAppender.ts b/src/vs/platform/telemetry/node/appInsightsAppender.ts index 0b5554ce5b7..d5fc5987438 100644 --- a/src/vs/platform/telemetry/node/appInsightsAppender.ts +++ b/src/vs/platform/telemetry/node/appInsightsAppender.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { mixin } from 'vs/base/common/objects'; import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; -async function getClient(aiKey: string, testCollector: boolean): Promise { +async function getClient(aiKey: string): Promise { const appInsights = await import('applicationinsights'); let client: TelemetryClient; if (appInsights.defaultClient) { @@ -29,7 +29,7 @@ async function getClient(aiKey: string, testCollector: boolean): Promise TelemetryClient), // allow factory function for testing - private readonly testCollector?: boolean, - private readonly mirrored?: boolean ) { if (!this._defaultData) { this._defaultData = Object.create(null); @@ -70,7 +68,7 @@ export class AppInsightsAppender implements ITelemetryAppender { } if (!this._asyncAIClient) { - this._asyncAIClient = getClient(this._aiClient, this.testCollector ?? false); + this._asyncAIClient = getClient(this._aiClient); } this._asyncAIClient.then( @@ -91,10 +89,6 @@ export class AppInsightsAppender implements ITelemetryAppender { data = mixin(data, this._defaultData); data = validateTelemetryData(data); - if (this.testCollector) { - data.properties['common.useragent'] = this.mirrored ? 'mirror-collector++' : 'collector++'; - } - // Attemps to suppress https://github.com/microsoft/vscode/issues/140624 try { this._withAIClient((aiClient) => aiClient.trackEvent({ diff --git a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts index 4e5d8adfd80..2b7a2228a4a 100644 --- a/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts +++ b/src/vs/platform/telemetry/node/customEndpointTelemetryService.ts @@ -8,6 +8,7 @@ import { Client as TelemetryClient } from 'vs/base/parts/ipc/node/ipc.cp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILoggerService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/common/telemetryIpc'; import { TelemetryLogAppender } from 'vs/platform/telemetry/common/telemetryLogAppender'; @@ -22,6 +23,7 @@ export class CustomEndpointTelemetryService implements ICustomEndpointTelemetryS @ITelemetryService private readonly telemetryService: ITelemetryService, @ILoggerService private readonly loggerService: ILoggerService, @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IProductService private readonly productService: IProductService ) { } private async getCustomTelemetryService(endpoint: ITelemetryEndpoint): Promise { @@ -54,7 +56,7 @@ export class CustomEndpointTelemetryService implements ICustomEndpointTelemetryS this.customTelemetryServices.set(endpoint.id, new TelemetryService({ appenders, sendErrorTelemetry: endpoint.sendErrorTelemetry - }, this.configurationService)); + }, this.configurationService, this.productService)); } return this.customTelemetryServices.get(endpoint.id)!; diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 63e9486ad99..9fbe21f1057 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -8,6 +8,8 @@ import * as sinonTest from 'sinon-test'; import * as Errors from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryData, TelemetryConfiguration, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; @@ -88,9 +90,11 @@ class ErrorTestingSettings { suite('TelemetryService', () => { + const TestProductService: IProductService = { _serviceBrand: undefined, ...product }; + test('Disposing', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); return service.publicLog('testPrivateEvent').then(() => { assert.strictEqual(testAppender.getEventsCount(), 1); @@ -103,7 +107,7 @@ suite('TelemetryService', () => { // event reporting test('Simple event', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent').then(_ => { assert.strictEqual(testAppender.getEventsCount(), 1); @@ -116,7 +120,7 @@ suite('TelemetryService', () => { test('Event with data', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent', { 'stringProp': 'property', @@ -144,7 +148,7 @@ suite('TelemetryService', () => { let service = new TelemetryService({ appenders: [testAppender], commonProperties: Promise.resolve({ foo: 'JA!', get bar() { return Math.random(); } }) - }, new TestConfigurationService()); + }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent').then(_ => { let [first] = testAppender.events; @@ -162,7 +166,7 @@ suite('TelemetryService', () => { let service = new TelemetryService({ appenders: [testAppender], commonProperties: Promise.resolve({ foo: 'JA!', get bar() { return Math.random(); } }) - }, new TestConfigurationService()); + }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent', { hightower: 'xl', price: 8000 }).then(_ => { let [first] = testAppender.events; @@ -184,7 +188,7 @@ suite('TelemetryService', () => { sessionID: 'one', ['common.machineId']: 'three', }) - }, new TestConfigurationService()); + }, new TestConfigurationService(), TestProductService); return service.getTelemetryInfo().then(info => { assert.strictEqual(info.sessionId, 'one'); @@ -196,7 +200,7 @@ suite('TelemetryService', () => { test('telemetry on by default', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent').then(() => { assert.strictEqual(testAppender.getEventsCount(), 1); @@ -211,7 +215,7 @@ suite('TelemetryService', () => { private promises: Promise[] = []; constructor(config: ITelemetryServiceConfig) { - super({ ...config, sendErrorTelemetry: true }, new TestConfigurationService); + super({ ...config, sendErrorTelemetry: true }, new TestConfigurationService, TestProductService); this.promises = this.promises ?? []; this.promises = this.promises ?? []; } @@ -773,7 +777,7 @@ suite('TelemetryService', () => { test('Telemetry Service sends events when telemetry is on', sinonTestFn(function () { let testAppender = new TestTelemetryAppender(); - let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService()); + let service = new TelemetryService({ appenders: [testAppender] }, new TestConfigurationService(), TestProductService); return service.publicLog('testEvent').then(() => { assert.strictEqual(testAppender.getEventsCount(), 1); @@ -794,7 +798,7 @@ suite('TelemetryService', () => { override getValue() { return telemetryLevel as any; } - }()); + }(), TestProductService); assert.strictEqual(service.telemetryLevel, TelemetryLevel.NONE); diff --git a/src/vs/workbench/contrib/terminal/common/capabilities/capabilities.ts b/src/vs/platform/terminal/common/capabilities/capabilities.ts similarity index 78% rename from src/vs/workbench/contrib/terminal/common/capabilities/capabilities.ts rename to src/vs/platform/terminal/common/capabilities/capabilities.ts index 1f462a9032b..6086093700c 100644 --- a/src/vs/workbench/contrib/terminal/common/capabilities/capabilities.ts +++ b/src/vs/platform/terminal/common/capabilities/capabilities.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { CwdDetectionCapability } from 'vs/workbench/contrib/terminal/common/capabilities/cwdDetectionCapability'; -import { NaiveCwdDetectionCapability } from 'vs/workbench/contrib/terminal/common/capabilities/naiveCwdDetectionCapability'; +import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; /** * Primarily driven by the shell integration feature, a terminal capability is the mechanism for @@ -69,15 +68,27 @@ export interface ITerminalCapabilityStore { * implementations. */ export interface ITerminalCapabilityImplMap { - [TerminalCapability.CwdDetection]: InstanceType; + [TerminalCapability.CwdDetection]: ICwdDetectionCapability; [TerminalCapability.CommandDetection]: ICommandDetectionCapability; - [TerminalCapability.NaiveCwdDetection]: InstanceType; + [TerminalCapability.NaiveCwdDetection]: INaiveCwdDetectionCapability; [TerminalCapability.PartialCommandDetection]: IPartialCommandDetectionCapability; } +export interface ICwdDetectionCapability { + readonly type: TerminalCapability.CwdDetection; + readonly onDidChangeCwd: Event; + readonly cwds: string[]; + getCwd(): string; + updateCwd(cwd: string): void; +} + export interface ICommandDetectionCapability { readonly type: TerminalCapability.CommandDetection; readonly commands: readonly ITerminalCommand[]; + /** The command currently being executed, otherwise undefined. */ + readonly executingCommand: string | undefined; + /** The current cwd at the cursor's position. */ + readonly cwd: string | undefined; readonly onCommandStarted: Event; readonly onCommandFinished: Event; setCwd(value: string): void; @@ -90,6 +101,8 @@ export interface ICommandDetectionCapability { handlePromptStart(): void; handleContinuationStart(): void; handleContinuationEnd(): void; + handleRightPromptStart(): void; + handleRightPromptEnd(): void; handleCommandStart(): void; handleCommandExecuted(): void; handleCommandFinished(exitCode: number | undefined): void; @@ -97,6 +110,14 @@ export interface ICommandDetectionCapability { * Set the command line explicitly. */ setCommandLine(commandLine: string): void; + serialize(): ISerializedCommandDetectionCapability; + deserialize(serialized: ISerializedCommandDetectionCapability): void; +} + +export interface INaiveCwdDetectionCapability { + readonly type: TerminalCapability.NaiveCwdDetection; + readonly onDidChangeCwd: Event; + getCwd(): Promise; } export interface IPartialCommandDetectionCapability { @@ -112,6 +133,8 @@ export interface ITerminalCommand { exitCode?: number; marker?: IXtermMarker; endMarker?: IXtermMarker; + executedMarker?: IXtermMarker; + commandStartLineContent?: string; getOutput(): string | undefined; hasOutput: boolean; } diff --git a/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts new file mode 100644 index 00000000000..bae59f94808 --- /dev/null +++ b/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts @@ -0,0 +1,478 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { timeout } from 'vs/base/common/async'; +import { Emitter } from 'vs/base/common/event'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { ISerializedCommand, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; +// Importing types is safe in any layer +// eslint-disable-next-line code-import-patterns +import type { IBuffer, IDisposable, IMarker, Terminal } from 'xterm-headless'; + +export interface ICurrentPartialCommand { + previousCommandMarker?: IMarker; + + promptStartMarker?: IMarker; + + commandStartMarker?: IMarker; + commandStartX?: number; + commandStartLineContent?: string; + + commandRightPromptStartX?: number; + commandRightPromptEndX?: number; + + commandLines?: IMarker; + + commandExecutedMarker?: IMarker; + commandExecutedX?: number; + + commandFinishedMarker?: IMarker; + + currentContinuationMarker?: IMarker; + continuations?: { marker: IMarker; end: number }[]; + + command?: string; +} + +interface ITerminalDimensions { + cols: number; + rows: number; +} + +export class CommandDetectionCapability implements ICommandDetectionCapability { + readonly type = TerminalCapability.CommandDetection; + + protected _commands: ITerminalCommand[] = []; + private _exitCode: number | undefined; + private _cwd: string | undefined; + private _currentCommand: ICurrentPartialCommand = {}; + private _isWindowsPty: boolean = false; + private _onCursorMoveListener?: IDisposable; + private _commandMarkers: IMarker[] = []; + private _dimensions: ITerminalDimensions; + + get commands(): readonly ITerminalCommand[] { return this._commands; } + get executingCommand(): string | undefined { return this._currentCommand.command; } + get cwd(): string | undefined { return this._cwd; } + + private readonly _onCommandStarted = new Emitter(); + readonly onCommandStarted = this._onCommandStarted.event; + private readonly _onCommandFinished = new Emitter(); + readonly onCommandFinished = this._onCommandFinished.event; + + constructor( + private readonly _terminal: Terminal, + @ILogService private readonly _logService: ILogService + ) { + this._dimensions = { + cols: this._terminal.cols, + rows: this._terminal.rows + }; + this._terminal.onResize(e => this._handleResize(e)); + } + + private _handleResize(e: { cols: number; rows: number }) { + if (this._isWindowsPty) { + this._preHandleResizeWindows(e); + } + this._dimensions.cols = e.cols; + this._dimensions.rows = e.rows; + } + + private _preHandleResizeWindows(e: { cols: number; rows: number }) { + // Resize behavior is different under conpty; instead of bringing parts of the scrollback + // back into the viewport, new lines are inserted at the bottom (ie. the same behavior as if + // there was no scrollback). + // + // On resize this workaround will wait for a conpty reprint to occur by waiting for the + // cursor to move, it will then calculate the number of lines that the commands within the + // viewport _may have_ shifted. After verifying the content of the current line is + // incorrect, the line after shifting is checked and if that matches delete events are fired + // on the xterm.js buffer to move the markers. + // + // While a bit hacky, this approach is quite safe and seems to work great at least for pwsh. + const baseY = this._terminal.buffer.active.baseY; + const rowsDifference = e.rows - this._dimensions.rows; + // Only do when rows increase, do in the next frame as this needs to happen after + // conpty reprints the screen + if (rowsDifference > 0) { + this._waitForCursorMove().then(() => { + // Calculate the number of lines the content may have shifted, this will max out at + // scrollback count since the standard behavior will be used then + const potentialShiftedLineCount = Math.min(rowsDifference, baseY); + // For each command within the viewport, assume commands are in the correct order + for (let i = this.commands.length - 1; i >= 0; i--) { + const command = this.commands[i]; + if (!command.marker || command.marker.line < baseY || command.commandStartLineContent === undefined) { + break; + } + const line = this._terminal.buffer.active.getLine(command.marker.line); + if (!line || line.translateToString(true) === command.commandStartLineContent) { + continue; + } + const shiftedY = command.marker.line - potentialShiftedLineCount; + const shiftedLine = this._terminal.buffer.active.getLine(shiftedY); + if (shiftedLine?.translateToString(true) !== command.commandStartLineContent) { + continue; + } + // HACK: xterm.js doesn't expose this by design as it's an internal core + // function an embedder could easily do damage with. Additionally, this + // can't really be upstreamed since the event relies on shell integration to + // verify the shifting is necessary. + (this._terminal as any)._core._bufferService.buffer.lines.onDeleteEmitter.fire({ + index: this._terminal.buffer.active.baseY, + amount: potentialShiftedLineCount + }); + } + }); + } + } + + private _waitForCursorMove(): Promise { + const cursorX = this._terminal.buffer.active.cursorX; + const cursorY = this._terminal.buffer.active.cursorY; + let totalDelay = 0; + return new Promise((resolve, reject) => { + const interval = setInterval(() => { + if (cursorX !== this._terminal.buffer.active.cursorX || cursorY !== this._terminal.buffer.active.cursorY) { + resolve(); + clearInterval(interval); + return; + } + totalDelay += 10; + if (totalDelay > 1000) { + clearInterval(interval); + resolve(); + } + }, 10); + }); + } + + setCwd(value: string) { + this._cwd = value; + } + + setIsWindowsPty(value: boolean) { + this._isWindowsPty = value; + } + + getCwdForLine(line: number): string | undefined { + // Handle the current partial command first, anything below it's prompt is considered part + // of the current command + if (this._currentCommand.promptStartMarker && line >= this._currentCommand.promptStartMarker?.line) { + return this._cwd; + } + // TODO: It would be more reliable to take the closest cwd above the line if it isn't found for the line + // TODO: Use a reverse for loop to find the line to avoid creating another array + const reversed = [...this._commands].reverse(); + return reversed.find(c => c.marker!.line <= line - 1)?.cwd; + } + + handlePromptStart(): void { + this._currentCommand.promptStartMarker = this._terminal.registerMarker(0); + this._logService.debug('CommandDetectionCapability#handlePromptStart', this._terminal.buffer.active.cursorX, this._currentCommand.promptStartMarker?.line); + } + + handleContinuationStart(): void { + this._currentCommand.currentContinuationMarker = this._terminal.registerMarker(0); + this._logService.debug('CommandDetectionCapability#handleContinuationStart', this._currentCommand.currentContinuationMarker); + } + + handleContinuationEnd(): void { + if (!this._currentCommand.currentContinuationMarker) { + this._logService.warn('CommandDetectionCapability#handleContinuationEnd Received continuation end without start'); + return; + } + if (!this._currentCommand.continuations) { + this._currentCommand.continuations = []; + } + this._currentCommand.continuations.push({ + marker: this._currentCommand.currentContinuationMarker, + end: this._terminal.buffer.active.cursorX + }); + this._currentCommand.currentContinuationMarker = undefined; + this._logService.debug('CommandDetectionCapability#handleContinuationEnd', this._currentCommand.continuations[this._currentCommand.continuations.length - 1]); + } + + handleRightPromptStart(): void { + this._currentCommand.commandRightPromptStartX = this._terminal.buffer.active.cursorX; + this._logService.debug('CommandDetectionCapability#handleRightPromptStart', this._currentCommand.commandRightPromptStartX); + } + + handleRightPromptEnd(): void { + this._currentCommand.commandRightPromptEndX = this._terminal.buffer.active.cursorX; + this._logService.debug('CommandDetectionCapability#handleRightPromptEnd', this._currentCommand.commandRightPromptEndX); + } + + handleCommandStart(): void { + if (this._isWindowsPty) { + this._handleCommandStartWindows(); + return; + } + this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX; + this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); + this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker } as ITerminalCommand); + this._logService.debug('CommandDetectionCapability#handleCommandStart', this._currentCommand.commandStartX, this._currentCommand.commandStartMarker?.line); + } + + private _handleCommandStartWindows(): void { + this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX; + + // On Windows track all cursor movements after the command start sequence + this._commandMarkers.length = 0; + // HACK: Fire command started on the following frame on Windows to allow the cursor + // position to update as conpty often prints the sequence on a different line to the + // actual line the command started on. + timeout(0).then(() => { + if (!this._currentCommand.commandExecutedMarker) { + this._onCursorMoveListener = this._terminal.onCursorMove(() => { + if (this._commandMarkers.length === 0 || this._commandMarkers[this._commandMarkers.length - 1].line !== this._terminal.buffer.active.cursorY) { + const marker = this._terminal.registerMarker(0); + if (marker) { + this._commandMarkers.push(marker); + } + } + }); + } + this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); + if (this._currentCommand.commandStartMarker) { + const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line); + if (line) { + this._currentCommand.commandStartLineContent = line.translateToString(true); + } + } + this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker } as ITerminalCommand); + this._logService.debug('CommandDetectionCapability#_handleCommandStartWindows', this._currentCommand.commandStartX, this._currentCommand.commandStartMarker?.line); + }); + } + + handleCommandExecuted(): void { + if (this._isWindowsPty) { + this._handleCommandExecutedWindows(); + return; + } + + this._currentCommand.commandExecutedMarker = this._terminal.registerMarker(0); + this._currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX; + this._logService.debug('CommandDetectionCapability#handleCommandExecuted', this._currentCommand.commandExecutedX, this._currentCommand.commandExecutedMarker?.line); + + // Sanity check optional props + if (!this._currentCommand.commandStartMarker || !this._currentCommand.commandExecutedMarker || this._currentCommand.commandStartX === undefined) { + return; + } + + // Calculate the command + this._currentCommand.command = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line)?.translateToString(true, this._currentCommand.commandStartX, this._currentCommand.commandRightPromptStartX).trim(); + let y = this._currentCommand.commandStartMarker.line + 1; + const commandExecutedLine = this._currentCommand.commandExecutedMarker.line; + for (; y < commandExecutedLine; y++) { + const line = this._terminal.buffer.active.getLine(y); + if (line) { + const continuation = this._currentCommand.continuations?.find(e => e.marker.line === y); + if (continuation) { + this._currentCommand.command += '\n'; + } + const startColumn = continuation?.end ?? 0; + this._currentCommand.command += line.translateToString(true, startColumn); + } + } + if (y === commandExecutedLine) { + this._currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, this._currentCommand.commandExecutedX) || ''; + } + } + + private _handleCommandExecutedWindows(): void { + // On Windows, use the gathered cursor move markers to correct the command start and + // executed markers + this._onCursorMoveListener?.dispose(); + this._onCursorMoveListener = undefined; + this._evaluateCommandMarkersWindows(); + this._currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX; + this._logService.debug('CommandDetectionCapability#handleCommandExecuted', this._currentCommand.commandExecutedX, this._currentCommand.commandExecutedMarker?.line); + } + + handleCommandFinished(exitCode: number | undefined): void { + if (this._isWindowsPty) { + this._preHandleCommandFinishedWindows(); + } + + this._currentCommand.commandFinishedMarker = this._terminal.registerMarker(0); + const command = this._currentCommand.command; + this._logService.debug('CommandDetectionCapability#handleCommandFinished', this._terminal.buffer.active.cursorX, this._currentCommand.commandFinishedMarker?.line, this._currentCommand.command, this._currentCommand); + this._exitCode = exitCode; + + // HACK: Handle a special case on some versions of bash where identical commands get merged + // in the output of `history`, this detects that case and sets the exit code to the the last + // command's exit code. This covered the majority of cases but will fail if the same command + // runs with a different exit code, that will need a more robust fix where we send the + // command ID and exit code over to the capability to adjust there. + if (this._exitCode === undefined) { + const lastCommand = this.commands.length > 0 ? this.commands[this.commands.length - 1] : undefined; + if (command && command.length > 0 && lastCommand?.command === command) { + this._exitCode = lastCommand.exitCode; + } + } + + if (this._currentCommand.commandStartMarker === undefined || !this._terminal.buffer.active) { + return; + } + + if (command !== undefined && !command.startsWith('\\')) { + const buffer = this._terminal.buffer.active; + const timestamp = Date.now(); + const executedMarker = this._currentCommand.commandExecutedMarker; + const endMarker = this._currentCommand.commandFinishedMarker; + const newCommand: ITerminalCommand = { + command, + marker: this._currentCommand.commandStartMarker, + endMarker, + executedMarker, + timestamp, + cwd: this._cwd, + exitCode: this._exitCode, + commandStartLineContent: this._currentCommand.commandStartLineContent, + hasOutput: !!(executedMarker && endMarker && executedMarker?.line < endMarker!.line), + getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer) + }; + this._commands.push(newCommand); + this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); + this._onCommandFinished.fire(newCommand); + } + this._currentCommand.previousCommandMarker = this._currentCommand.commandStartMarker; + this._currentCommand = {}; + } + + private _preHandleCommandFinishedWindows(): void { + if (this._currentCommand.commandExecutedMarker) { + return; + } + // This is done on command finished just in case command executed never happens (for example + // PSReadLine tab completion) + if (this._commandMarkers.length === 0) { + // If the command start timeout doesn't happen before command finished, just use the + // current marker. + if (!this._currentCommand.commandStartMarker) { + this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); + } + if (this._currentCommand.commandStartMarker) { + this._commandMarkers.push(this._currentCommand.commandStartMarker); + } + } + this._evaluateCommandMarkersWindows(); + } + + private _evaluateCommandMarkersWindows(): void { + // On Windows, use the gathered cursor move markers to correct the command start and + // executed markers. + if (this._commandMarkers.length === 0) { + return; + } + this._commandMarkers = this._commandMarkers.sort((a, b) => a.line - b.line); + this._currentCommand.commandStartMarker = this._commandMarkers[0]; + if (this._currentCommand.commandStartMarker) { + const line = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line); + if (line) { + this._currentCommand.commandStartLineContent = line.translateToString(true); + } + } + this._currentCommand.commandExecutedMarker = this._commandMarkers[this._commandMarkers.length - 1]; + } + + setCommandLine(commandLine: string) { + this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine); + this._currentCommand.command = commandLine; + } + + serialize(): ISerializedCommandDetectionCapability { + const commands: ISerializedCommand[] = this.commands.map(e => { + return { + startLine: e.marker?.line, + startX: undefined, + endLine: e.endMarker?.line, + executedLine: e.executedMarker?.line, + command: e.command, + cwd: e.cwd, + exitCode: e.exitCode, + commandStartLineContent: e.commandStartLineContent, + timestamp: e.timestamp + }; + }); + if (this._currentCommand.commandStartMarker) { + commands.push({ + startLine: this._currentCommand.commandStartMarker.line, + startX: this._currentCommand.commandStartX, + endLine: undefined, + executedLine: undefined, + command: '', + cwd: this._cwd, + exitCode: undefined, + commandStartLineContent: undefined, + timestamp: 0, + }); + } + return { + isWindowsPty: this._isWindowsPty, + commands + }; + } + + deserialize(serialized: ISerializedCommandDetectionCapability): void { + if (serialized.isWindowsPty) { + this.setIsWindowsPty(serialized.isWindowsPty); + } + const buffer = this._terminal.buffer.normal; + for (const e of serialized.commands) { + const marker = e.startLine !== undefined ? this._terminal.registerMarker(e.startLine - (buffer.baseY + buffer.cursorY)) : undefined; + // Check for invalid command + if (!marker) { + continue; + } + // Partial command + if (!e.endLine) { + this._currentCommand.commandStartMarker = marker; + this._currentCommand.commandStartX = e.startX; + this._cwd = e.cwd; + this._onCommandStarted.fire({ marker } as ITerminalCommand); + continue; + } + // Full command + const endMarker = e.endLine !== undefined ? this._terminal.registerMarker(e.endLine - (buffer.baseY + buffer.cursorY)) : undefined; + const executedMarker = e.executedLine !== undefined ? this._terminal.registerMarker(e.executedLine - (buffer.baseY + buffer.cursorY)) : undefined; + const newCommand = { + command: e.command, + marker, + endMarker, + executedMarker, + timestamp: e.timestamp, + cwd: e.cwd, + commandStartLineContent: e.commandStartLineContent, + exitCode: e.exitCode, + hasOutput: !!(executedMarker && endMarker && executedMarker.line < endMarker.line), + getOutput: () => getOutputForCommand(executedMarker, endMarker, buffer) + }; + this._commands.push(newCommand); + this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); + this._onCommandFinished.fire(newCommand); + } + } +} + +function getOutputForCommand(executedMarker: IMarker | undefined, endMarker: IMarker | undefined, buffer: IBuffer): string | undefined { + if (!executedMarker || !endMarker) { + return undefined; + } + const startLine = executedMarker.line; + const endLine = endMarker.line; + + if (startLine === endLine) { + return undefined; + } + let output = ''; + for (let i = startLine; i < endLine; i++) { + output += buffer.getLine(i)?.translateToString() + '\n'; + } + return output === '' ? undefined : output; +} diff --git a/src/vs/workbench/contrib/terminal/common/capabilities/cwdDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/cwdDetectionCapability.ts similarity index 86% rename from src/vs/workbench/contrib/terminal/common/capabilities/cwdDetectionCapability.ts rename to src/vs/platform/terminal/common/capabilities/cwdDetectionCapability.ts index 4c312135f42..7e3fcfa4a5d 100644 --- a/src/vs/workbench/contrib/terminal/common/capabilities/cwdDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/cwdDetectionCapability.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ICwdDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -export class CwdDetectionCapability { +export class CwdDetectionCapability implements ICwdDetectionCapability { readonly type = TerminalCapability.CwdDetection; private _cwd = ''; private _cwds = new Map(); diff --git a/src/vs/workbench/contrib/terminal/common/capabilities/naiveCwdDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability.ts similarity index 82% rename from src/vs/workbench/contrib/terminal/common/capabilities/naiveCwdDetectionCapability.ts rename to src/vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability.ts index ca40c5ef2d1..daab3687d42 100644 --- a/src/vs/workbench/contrib/terminal/common/capabilities/naiveCwdDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability.ts @@ -5,9 +5,9 @@ import { Emitter } from 'vs/base/common/event'; import { ITerminalChildProcess } from 'vs/platform/terminal/common/terminal'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { TerminalCapability, INaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; -export class NaiveCwdDetectionCapability { +export class NaiveCwdDetectionCapability implements INaiveCwdDetectionCapability { constructor(private readonly _process: ITerminalChildProcess) { } readonly type = TerminalCapability.NaiveCwdDetection; private _cwd = ''; diff --git a/src/vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability.ts b/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts similarity index 83% rename from src/vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability.ts rename to src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts index e349ac103c5..1a722cd14da 100644 --- a/src/vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability.ts +++ b/src/vs/platform/terminal/common/capabilities/partialCommandDetectionCapability.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; -import { IPartialCommandDetectionCapability, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { IMarker, Terminal } from 'xterm'; +import { IPartialCommandDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +// Importing types is safe in any layer +// eslint-disable-next-line code-import-patterns +import { IMarker, Terminal } from 'xterm-headless'; const enum Constants { /** @@ -31,11 +33,11 @@ export class PartialCommandDetectionCapability implements IPartialCommandDetecti constructor( private readonly _terminal: Terminal, ) { - this._terminal.onKey(e => this._onKey(e.key)); + this._terminal.onData(e => this._onData(e)); } - private _onKey(key: string): void { - if (key === '\x0d') { + private _onData(data: string): void { + if (data === '\x0d') { this._onEnter(); } } diff --git a/src/vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore.ts b/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts similarity index 97% rename from src/vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore.ts rename to src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts index 155efd60f9c..69d51539258 100644 --- a/src/vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore.ts +++ b/src/vs/platform/terminal/common/capabilities/terminalCapabilityStore.ts @@ -5,7 +5,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ITerminalCapabilityImplMap, ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityImplMap, ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; export class TerminalCapabilityStore extends Disposable implements ITerminalCapabilityStore { private _map: Map = new Map(); diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index e93d2556287..32247de9d60 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -7,7 +7,8 @@ import { Event } from 'vs/base/common/event'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; +import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; export const enum TerminalSettingPrefix { @@ -111,13 +112,23 @@ export const enum TerminalSettingId { ShellIntegrationCommandHistory = 'terminal.integrated.shellIntegration.history' } +export const enum PosixShellType { + PowerShell = 'pwsh', + Bash = 'bash', + Fish = 'fish', + Sh = 'sh', + Csh = 'csh', + Ksh = 'ksh', + Zsh = 'zsh', +} export const enum WindowsShellType { CommandPrompt = 'cmd', PowerShell = 'pwsh', Wsl = 'wsl', GitBash = 'gitbash' } -export type TerminalShellType = WindowsShellType | undefined; +export type TerminalShellType = PosixShellType | WindowsShellType | undefined; + export interface IRawTerminalInstanceLayoutInfo { relativeSize: number; terminal: T; @@ -263,7 +274,7 @@ export interface IPtyService extends IPtyHostController { unicodeVersion: '6' | '11', env: IProcessEnvironment, executableEnv: IProcessEnvironment, - windowsEnableConpty: boolean, + options: ITerminalProcessOptions, shouldPersist: boolean, workspaceId: string, workspaceName: string @@ -331,16 +342,16 @@ export interface ISerializedTerminalState { id: number; shellLaunchConfig: IShellLaunchConfig; processDetails: IProcessDetails; - processLaunchOptions: IPersistentTerminalProcessLaunchOptions; + processLaunchConfig: IPersistentTerminalProcessLaunchConfig; unicodeVersion: '6' | '11'; replayEvent: IPtyHostProcessReplayEvent; timestamp: number; } -export interface IPersistentTerminalProcessLaunchOptions { +export interface IPersistentTerminalProcessLaunchConfig { env: IProcessEnvironment; executableEnv: IProcessEnvironment; - windowsEnableConpty: boolean; + options: ITerminalProcessOptions; } export interface IRequestResolveVariablesEvent { @@ -539,6 +550,18 @@ export interface IShellLaunchConfigDto { hideFromUser?: boolean; } +/** + * A set of options for the terminal process. These differ from the shell launch config in that they + * are set internally to the terminal component, not from the outside. + */ +export interface ITerminalProcessOptions { + shellIntegration: { + enabled: boolean; + showWelcome: boolean; + }; + windowsEnableConpty: boolean; +} + export interface ITerminalEnvironment { [key: string]: string | null | undefined; } @@ -575,6 +598,7 @@ export interface ITerminalChildProcess { onProcessReady: Event; onDidChangeProperty: Event>; onProcessExit: Event; + onRestoreCommands?: Event; /** * Starts the process. @@ -746,3 +770,8 @@ export interface IExtensionTerminalProfile extends ITerminalProfileContribution export type ITerminalProfileObject = ITerminalExecutable | ITerminalProfileSource | IExtensionTerminalProfile | null; export type ITerminalProfileType = ITerminalProfile | IExtensionTerminalProfile; + +export interface IShellIntegration { + capabilities: ITerminalCapabilityStore; + deserialize(serialized: ISerializedCommandDetectionCapability): void; +} diff --git a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts index 25e0f2b934f..9dc40ebec2c 100644 --- a/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts +++ b/src/vs/platform/terminal/common/terminalPlatformConfiguration.ts @@ -130,7 +130,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.AutomationProfileLinux]: { restricted: true, - markdownDescription: localize('terminal.integrated.automationProfile.linux', "The terminal profile to use on Linux for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.linux#'), + markdownDescription: localize('terminal.integrated.automationProfile.linux', "The terminal profile to use on Linux for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '`#terminal.integrated.automationShell.linux#`'), type: ['object', 'null'], default: null, 'anyOf': [ @@ -148,7 +148,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.AutomationProfileMacOs]: { restricted: true, - description: localize('terminal.integrated.automationProfile.osx', "The terminal profile to use on macOS for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.osx#'), + markdownDescription: localize('terminal.integrated.automationProfile.osx', "The terminal profile to use on macOS for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '`#terminal.integrated.automationShell.osx#`'), type: ['object', 'null'], default: null, 'anyOf': [ @@ -166,7 +166,7 @@ const terminalPlatformConfiguration: IConfigurationNode = { }, [TerminalSettingId.AutomationProfileWindows]: { restricted: true, - description: localize('terminal.integrated.automationProfile.windows', "The terminal profile to use for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '#terminal.integrated.automationShell.windows#'), + markdownDescription: localize('terminal.integrated.automationProfile.windows', "The terminal profile to use for automation-related terminal usage like tasks and debug. This setting will currently be ignored if {0} is set.", '`#terminal.integrated.automationShell.windows#`'), type: ['object', 'null'], default: null, 'anyOf': [ diff --git a/src/vs/platform/terminal/common/terminalProcess.ts b/src/vs/platform/terminal/common/terminalProcess.ts index d32d309235b..dea87c461a1 100644 --- a/src/vs/platform/terminal/common/terminalProcess.ts +++ b/src/vs/platform/terminal/common/terminalProcess.ts @@ -63,7 +63,27 @@ export interface IProcessDetails { export type ITerminalTabLayoutInfoDto = IRawTerminalTabLayoutInfo; -export interface ReplayEntry { cols: number; rows: number; data: string } +export interface ReplayEntry { + cols: number; + rows: number; + data: string; +} +export interface ISerializedCommand { + command: string; + cwd: string | undefined; + startLine: number | undefined; + startX: number | undefined; + endLine: number | undefined; + executedLine: number | undefined; + exitCode: number | undefined; + commandStartLineContent: string | undefined; + timestamp: number; +} +export interface ISerializedCommandDetectionCapability { + isWindowsPty: boolean; + commands: ISerializedCommand[]; +} export interface IPtyHostProcessReplayEvent { events: ReplayEntry[]; + commands: ISerializedCommandDetectionCapability; } diff --git a/src/vs/platform/terminal/common/terminalRecorder.ts b/src/vs/platform/terminal/common/terminalRecorder.ts index b0fb739f9e5..ee28528781e 100644 --- a/src/vs/platform/terminal/common/terminalRecorder.ts +++ b/src/vs/platform/terminal/common/terminalRecorder.ts @@ -84,7 +84,12 @@ export class TerminalRecorder { } }); return { - events: this._entries.map(entry => ({ cols: entry.cols, rows: entry.rows, data: entry.data[0] ?? '' })) + events: this._entries.map(entry => ({ cols: entry.cols, rows: entry.rows, data: entry.data[0] ?? '' })), + // No command restoration is needed when relaunching terminals + commands: { + isWindowsPty: false, + commands: [] + } }; } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts similarity index 74% rename from src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts rename to src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index bbc240954cd..180904b1ca3 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITerminalAddon, Terminal } from 'xterm'; -import { IShellIntegration } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellIntegration } from 'vs/platform/terminal/common/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; -import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CwdDetectionCapability } from 'vs/workbench/contrib/terminal/common/capabilities/cwdDetectionCapability'; -import { ICommandDetectionCapability, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { PartialCommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; +import { CwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/cwdDetectionCapability'; +import { ICommandDetectionCapability, ICwdDetectionCapability, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { PartialCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/partialCommandDetectionCapability'; +import { ILogService } from 'vs/platform/log/common/log'; +// Importing types is safe in any layer +// eslint-disable-next-line code-import-patterns +import type { ITerminalAddon, Terminal } from 'xterm-headless'; +import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; /** * Shell integration is a feature that enhances the terminal's understanding of what's happening @@ -95,6 +98,16 @@ const enum VSCodeOscPt { */ ContinuationEnd = 'G', + /** + * The start of the right prompt. + */ + RightPromptStart = 'H', + + /** + * The end of the right prompt. + */ + RightPromptEnd = 'I', + /** * Set an arbitrary property: `OSC 633 ; P ; = ST`, only known properties will * be handled. @@ -112,7 +125,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati readonly capabilities = new TerminalCapabilityStore(); constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService + @ILogService private readonly _logService: ILogService ) { super(); } @@ -165,6 +178,14 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._createOrGetCommandDetection(this._terminal).handleContinuationEnd(); return true; } + case VSCodeOscPt.RightPromptStart: { + this._createOrGetCommandDetection(this._terminal).handleRightPromptStart(); + return true; + } + case VSCodeOscPt.RightPromptEnd: { + this._createOrGetCommandDetection(this._terminal).handleRightPromptEnd(); + return true; + } case VSCodeOscPt.Property: { const [key, value] = args[0].split('='); switch (key) { @@ -188,7 +209,25 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return false; } - protected _createOrGetCwdDetection(): CwdDetectionCapability { + serialize(): ISerializedCommandDetectionCapability { + if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { + return { + isWindowsPty: false, + commands: [] + }; + } + const result = this._createOrGetCommandDetection(this._terminal).serialize(); + return result; + } + + deserialize(serialized: ISerializedCommandDetectionCapability): void { + if (!this._terminal) { + throw new Error('Cannot restore commands before addon is activated'); + } + this._createOrGetCommandDetection(this._terminal).deserialize(serialized); + } + + protected _createOrGetCwdDetection(): ICwdDetectionCapability { let cwdDetection = this.capabilities.get(TerminalCapability.CwdDetection); if (!cwdDetection) { cwdDetection = new CwdDetectionCapability(); @@ -200,7 +239,7 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati protected _createOrGetCommandDetection(terminal: Terminal): ICommandDetectionCapability { let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); if (!commandDetection) { - commandDetection = this._instantiationService.createInstance(CommandDetectionCapability, terminal); + commandDetection = new CommandDetectionCapability(terminal, this._logService); this.capabilities.add(TerminalCapability.CommandDetection, commandDetection); } return commandDetection; diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 911f3257aaf..924ee3c81b5 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -12,11 +12,11 @@ import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { parsePtyHostPort } from 'vs/platform/environment/common/environmentService'; -import { getResolvedShellEnv } from 'vs/platform/terminal/node/shellEnv'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService } from 'vs/platform/log/common/log'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, TerminalSettingId, ISerializedTerminalState } from 'vs/platform/terminal/common/terminal'; +import { HeartbeatConstants, IHeartbeatService, IProcessDataEvent, IPtyService, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, TerminalIcon, TerminalIpcChannels, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, TerminalSettingId, ISerializedTerminalState, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { registerTerminalPlatformConfiguration } from 'vs/platform/terminal/common/terminalPlatformConfiguration'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { detectAvailableProfiles } from 'vs/platform/terminal/node/terminalProfiles'; @@ -201,9 +201,21 @@ export class PtyHostService extends Disposable implements IPtyService { super.dispose(); } - async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean, workspaceId: string, workspaceName: string): Promise { + async createProcess( + shellLaunchConfig: IShellLaunchConfig, + cwd: string, + cols: number, + rows: number, + unicodeVersion: '6' | '11', + env: IProcessEnvironment, + executableEnv: IProcessEnvironment, + options: ITerminalProcessOptions, + shouldPersist: boolean, + workspaceId: string, + workspaceName: string + ): Promise { const timeout = setTimeout(() => this._handleUnresponsiveCreateProcess(), HeartbeatConstants.CreateProcessTimeout); - const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, windowsEnableConpty, shouldPersist, workspaceId, workspaceName); + const id = await this._proxy.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, options, shouldPersist, workspaceId, workspaceName); clearTimeout(timeout); lastPtyId = Math.max(lastPtyId, id); return id; diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 874a0184c81..0b4301c23e6 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -12,7 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { getSystemShell } from 'vs/base/node/shell'; import { ILogService } from 'vs/platform/log/common/log'; import { RequestStore } from 'vs/platform/terminal/common/requestStore'; -import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, IPersistentTerminalProcessLaunchOptions, ICrossVersionSerializedTerminalState, ISerializedTerminalState } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IPtyService, IRawTerminalInstanceLayoutInfo, IReconnectConstants, IRequestResolveVariablesEvent, IShellLaunchConfig, ITerminalInstanceLayoutInfoById, ITerminalLaunchError, ITerminalsLayoutInfo, ITerminalTabLayoutInfoById, TerminalIcon, IProcessProperty, TitleEventSource, ProcessPropertyType, IProcessPropertyMap, IFixedTerminalDimensions, IPersistentTerminalProcessLaunchConfig, ICrossVersionSerializedTerminalState, ISerializedTerminalState, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { escapeNonWindowsPath } from 'vs/platform/terminal/common/terminalEnvironment'; import { Terminal as XtermTerminal } from 'xterm-headless'; @@ -24,6 +24,8 @@ import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; import { localize } from 'vs/nls'; import { ignoreProcessNames } from 'vs/platform/terminal/node/childProcessMonitor'; import { TerminalAutoResponder } from 'vs/platform/terminal/common/terminalAutoResponder'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; +import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; type WorkspaceId = string; @@ -109,7 +111,7 @@ export class PtyService extends Disposable implements IPtyService { id: persistentProcessId, shellLaunchConfig: persistentProcess.shellLaunchConfig, processDetails: await this._buildProcessDetails(persistentProcessId, persistentProcess), - processLaunchOptions: persistentProcess.processLaunchOptions, + processLaunchConfig: persistentProcess.processLaunchOptions, unicodeVersion: persistentProcess.unicodeVersion, replayEvent: await persistentProcess.serializeNormalBuffer(), timestamp: Date.now() @@ -143,9 +145,9 @@ export class PtyService extends Disposable implements IPtyService { terminal.replayEvent.events[0].cols, terminal.replayEvent.events[0].rows, terminal.unicodeVersion, - terminal.processLaunchOptions.env, - terminal.processLaunchOptions.executableEnv, - terminal.processLaunchOptions.windowsEnableConpty, + terminal.processLaunchConfig.env, + terminal.processLaunchConfig.executableEnv, + terminal.processLaunchConfig.options, true, terminal.processDetails.workspaceId, terminal.processDetails.workspaceName, @@ -168,7 +170,7 @@ export class PtyService extends Disposable implements IPtyService { unicodeVersion: '6' | '11', env: IProcessEnvironment, executableEnv: IProcessEnvironment, - windowsEnableConpty: boolean, + options: ITerminalProcessOptions, shouldPersist: boolean, workspaceId: string, workspaceName: string, @@ -178,12 +180,12 @@ export class PtyService extends Disposable implements IPtyService { throw new Error('Attempt to create a process when attach object was provided'); } const id = ++this._lastPtyId; - const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, windowsEnableConpty, this._logService); + const process = new TerminalProcess(shellLaunchConfig, cwd, cols, rows, env, executableEnv, options, this._logService); process.onProcessData(event => this._onProcessData.fire({ id, event })); - const processLaunchOptions: IPersistentTerminalProcessLaunchOptions = { + const processLaunchOptions: IPersistentTerminalProcessLaunchConfig = { env, executableEnv, - windowsEnableConpty + options }; const persistentProcess = new PersistentTerminalProcess(id, process, workspaceId, workspaceName, shouldPersist, cols, rows, processLaunchOptions, unicodeVersion, this._reconnectConstants, this._logService, isReviving ? shellLaunchConfig.initialText : undefined, shellLaunchConfig.icon, shellLaunchConfig.color, shellLaunchConfig.name, shellLaunchConfig.fixedDimensions); process.onDidChangeProperty(property => this._onDidChangeProperty.fire({ id, property })); @@ -399,7 +401,7 @@ export class PtyService extends Disposable implements IPtyService { private _throwIfNoPty(id: number): PersistentTerminalProcess { const pty = this._ptys.get(id); if (!pty) { - throw new Error(`Could not find pty on pty host`); + throw new ErrorNoTelemetry(`Could not find pty on pty host`); } return pty; } @@ -478,7 +480,7 @@ export class PersistentTerminalProcess extends Disposable { readonly shouldPersistTerminal: boolean, cols: number, rows: number, - readonly processLaunchOptions: IPersistentTerminalProcessLaunchOptions, + readonly processLaunchOptions: IPersistentTerminalProcessLaunchConfig, public unicodeVersion: '6' | '11', reconnectConstants: IReconnectConstants, private readonly _logService: ILogService, @@ -499,7 +501,8 @@ export class PersistentTerminalProcess extends Disposable { rows, reconnectConstants.scrollback, unicodeVersion, - reviveBuffer + reviveBuffer, + this._logService ); this._fixedDimensions = fixedDimensions; this._orphanQuestionBarrier = null; @@ -722,35 +725,25 @@ export class PersistentTerminalProcess extends Disposable { } class XtermSerializer implements ITerminalSerializer { - private _xterm: XtermTerminal; + private readonly _xterm: XtermTerminal; + private readonly _shellIntegrationAddon: ShellIntegrationAddon; private _unicodeAddon?: XtermUnicode11Addon; - private _shellIntegrationEnabled: boolean = false; constructor( cols: number, rows: number, scrollback: number, unicodeVersion: '6' | '11', - reviveBuffer: string | undefined + reviveBuffer: string | undefined, + logService: ILogService ) { this._xterm = new XtermTerminal({ cols, rows, scrollback }); if (reviveBuffer) { this._xterm.writeln(reviveBuffer); - if (this._shellIntegrationEnabled) { - this._xterm.write('\x1b033]133;E\x1b007'); - } } - this._xterm.parser.registerOscHandler(133, (data => this._handleShellIntegration(data))); this.setUnicodeVersion(unicodeVersion); - } - - private _handleShellIntegration(data: string): boolean { - const [command,] = data.split(';'); - if (command === 'E') { - this._shellIntegrationEnabled = true; - return true; - } - return false; + this._shellIntegrationAddon = new ShellIntegrationAddon(logService); + this._xterm.loadAddon(this._shellIntegrationAddon); } handleData(data: string): void { @@ -777,7 +770,8 @@ class XtermSerializer implements ITerminalSerializer { rows: this._xterm.getOption('rows'), data: serialized } - ] + ], + commands: this._shellIntegrationAddon.serialize() }; } diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 09e72b3b1f8..dcbff469770 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as os from 'os'; +import { FileAccess } from 'vs/base/common/network'; import { getCaseInsensitive } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import * as process from 'vs/base/common/process'; +import { format } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; import * as pfs from 'vs/base/node/pfs'; +import { IShellLaunchConfig, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; export function getWindowsBuildNumber(): number { const osVersion = (/(\d+)\.(\d+)\.(\d+)/g).exec(os.release()); @@ -72,3 +75,175 @@ export async function findExecutable(command: string, cwd?: string, paths?: stri const fullPath = path.join(cwd, command); return await exists(fullPath) ? fullPath : undefined; } + +export interface IShellIntegrationConfigInjection { + /** + * A new set of arguments to use. + */ + newArgs: string[] | undefined; + /** + * An optional environment to mixing to the real environment. + */ + envMixin?: IProcessEnvironment; + /** + * An optional array of files to copy from `source` to `dest`. + */ + filesToCopy?: { + source: string; + dest: string; + }[]; +} + +/** + * For a given shell launch config, returns arguments to replace and an optional environment to + * mixin to the SLC's environment to enable shell integration. This must be run within the context + * that creates the process to ensure accuracy. Returns undefined if shell integration cannot be + * enabled. + */ +export function getShellIntegrationInjection( + shellLaunchConfig: IShellLaunchConfig, + options: ITerminalProcessOptions['shellIntegration'] +): IShellIntegrationConfigInjection | undefined { + // Shell integration arg injection is disabled when: + // - The global setting is disabled + // - There is no executable (not sure what script to run) + // - The terminal is used by a feature like tasks or debugging + if (!options.enabled || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal) { + return undefined; + } + + const originalArgs = shellLaunchConfig.args; + const shell = process.platform === 'win32' ? path.basename(shellLaunchConfig.executable).toLowerCase() : path.basename(shellLaunchConfig.executable); + const appRoot = path.dirname(FileAccess.asFileUri('', require).fsPath); + let newArgs: string[] | undefined; + + // Windows + if (isWindows) { + if (shell === 'pwsh.exe') { + if (!originalArgs || arePwshImpliedArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwsh); + } else if (arePwshLoginArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwshLogin); + } + if (!newArgs) { + return undefined; + } + if (newArgs) { + const additionalArgs = options.showWelcome ? '' : ' -HideWelcome'; + newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, additionalArgs); + } + return { newArgs }; + } + return undefined; + } + + // Linux & macOS + const envMixin: IProcessEnvironment = {}; + switch (shell) { + case 'bash': { + if (!originalArgs || originalArgs.length === 0) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); + } else if (areZshBashLoginArgs(originalArgs)) { + envMixin['VSCODE_SHELL_LOGIN'] = '1'; + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); + } + if (!newArgs) { + return undefined; + } + newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + if (!options.showWelcome) { + envMixin['VSCODE_SHELL_HIDE_WELCOME'] = '1'; + } + return { newArgs, envMixin }; + } + case 'pwsh': { + if (!originalArgs || arePwshImpliedArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Pwsh); + } else if (arePwshLoginArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.PwshLogin); + } + if (!newArgs) { + return undefined; + } + const additionalArgs = options.showWelcome ? '' : ' -HideWelcome'; + newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, additionalArgs); + return { newArgs }; + } + case 'zsh': { + if (!originalArgs || originalArgs.length === 0) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh); + } else if (areZshBashLoginArgs(originalArgs)) { + newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin); + } else if (originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh) || originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin)) { + newArgs = originalArgs; + } + if (!newArgs) { + return undefined; + } + newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array + newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot); + // Move .zshrc into $ZDOTDIR as the way to activate the script + const zdotdir = path.join(os.tmpdir(), 'vscode-zsh'); + envMixin['ZDOTDIR'] = zdotdir; + const filesToCopy: IShellIntegrationConfigInjection['filesToCopy'] = []; + filesToCopy.push({ + source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh'), + dest: path.join(zdotdir, '.zshrc') + }); + if (!options.showWelcome) { + envMixin['VSCODE_SHELL_HIDE_WELCOME'] = '1'; + } + return { newArgs, envMixin, filesToCopy }; + } + } + + return undefined; +} + +export enum ShellIntegrationExecutable { + WindowsPwsh = 'windows-pwsh', + WindowsPwshLogin = 'windows-pwsh-login', + Pwsh = 'pwsh', + PwshLogin = 'pwsh-login', + Zsh = 'zsh', + ZshLogin = 'zsh-login', + Bash = 'bash' +} + +export const shellIntegrationArgs: Map = new Map(); +shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwsh, ['-noexit', '-command', '. \"{0}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1\"{1}']); +shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwshLogin, ['-l', '-noexit', '-command', '. \"{0}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1\"{1}']); +shellIntegrationArgs.set(ShellIntegrationExecutable.Pwsh, ['-noexit', '-command', '. "{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"{1}']); +shellIntegrationArgs.set(ShellIntegrationExecutable.PwshLogin, ['-l', '-noexit', '-command', '. "{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"']); +shellIntegrationArgs.set(ShellIntegrationExecutable.Zsh, ['-i']); +shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']); +shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, ['--init-file', '{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh']); +const loginArgs = ['-login', '-l']; +const pwshImpliedArgs = ['-nol', '-nologo']; + +function arePwshLoginArgs(originalArgs: string | string[]): boolean { + if (typeof originalArgs === 'string') { + return loginArgs.includes(originalArgs.toLowerCase()); + } else { + return originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()) || + (originalArgs.length === 2 && + (((loginArgs.includes(originalArgs[0].toLowerCase())) || loginArgs.includes(originalArgs[1].toLowerCase()))) + && ((pwshImpliedArgs.includes(originalArgs[0].toLowerCase())) || pwshImpliedArgs.includes(originalArgs[1].toLowerCase()))); + } +} + +function arePwshImpliedArgs(originalArgs: string | string[]): boolean { + if (typeof originalArgs === 'string') { + return pwshImpliedArgs.includes(originalArgs.toLowerCase()); + } else { + return originalArgs.length === 0 || originalArgs?.length === 1 && pwshImpliedArgs.includes(originalArgs[0].toLowerCase()); + } +} + +function areZshBashLoginArgs(originalArgs: string | string[]): boolean { + return originalArgs === 'string' && loginArgs.includes(originalArgs.toLowerCase()) + || typeof originalArgs !== 'string' && originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()); +} diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 71db32abfce..a7c1d444d4e 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -17,9 +17,9 @@ import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; import { localize } from 'vs/nls'; import { ILogService } from 'vs/platform/log/common/log'; -import { FlowControlConstants, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; +import { FlowControlConstants, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap as IProcessPropertyMap, ProcessPropertyType, TerminalShellType, IProcessReadyEvent, ITerminalProcessOptions, PosixShellType } from 'vs/platform/terminal/common/terminal'; import { ChildProcessMonitor } from 'vs/platform/terminal/node/childProcessMonitor'; -import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; +import { findExecutable, getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; const enum ShutdownConstants { @@ -75,6 +75,16 @@ interface IWriteObject { isBinary: boolean; } +const posixShellTypeMap = new Map([ + ['bash', PosixShellType.Bash], + ['csh', PosixShellType.Csh], + ['fish', PosixShellType.Fish], + ['ksh', PosixShellType.Ksh], + ['sh', PosixShellType.Sh], + ['pwsh', PosixShellType.PowerShell], + ['zsh', PosixShellType.Zsh] +]); + export class TerminalProcess extends Disposable implements ITerminalChildProcess { readonly id = 0; readonly shouldPersist = false; @@ -111,7 +121,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess get exitMessage(): string | undefined { return this._exitMessage; } get currentTitle(): string { return this._windowsShellHelper?.shellTitle || this._currentTitle; } - get shellType(): TerminalShellType { return this._windowsShellHelper ? this._windowsShellHelper.shellType : undefined; } + get shellType(): TerminalShellType { return isWindows ? this._windowsShellHelper?.shellType : posixShellTypeMap.get(this._currentTitle); } private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; @@ -132,7 +142,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess * environment used for `findExecutable` */ private readonly _executableEnv: IProcessEnvironment, - windowsEnableConpty: boolean, + private readonly _options: ITerminalProcessOptions, @ILogService private readonly _logService: ILogService ) { super(); @@ -147,7 +157,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess this._initialCwd = cwd; this._properties[ProcessPropertyType.InitialCwd] = this._initialCwd; this._properties[ProcessPropertyType.Cwd] = this._initialCwd; - const useConpty = windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; + const useConpty = this._options.windowsEnableConpty && process.platform === 'win32' && getWindowsBuildNumber() >= 18309; this._ptyOptions = { name, cwd, @@ -187,6 +197,27 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return firstError; } + let injection: IShellIntegrationConfigInjection | undefined; + if (this._options.shellIntegration) { + injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options.shellIntegration); + if (!injection) { + this._logService.warn(`Shell integration cannot be enabled for executable "${this.shellLaunchConfig.executable}" and args`, this.shellLaunchConfig.args); + } else { + if (injection.envMixin) { + for (const [key, value] of Object.entries(injection.envMixin)) { + this._ptyOptions.env ||= {}; + this._ptyOptions.env[key] = value; + } + } + if (injection.filesToCopy) { + for (const f of injection.filesToCopy) { + await fs.mkdir(path.dirname(f.dest), { recursive: true }); + await fs.copyFile(f.source, f.dest); + } + } + } + } + // Handle zsh shell integration - Set $ZDOTDIR to a temp dir and create $ZDOTDIR/.zshrc if (this.shellLaunchConfig.env?.['_ZDOTDIR'] === '1') { const zdotdir = path.join(tmpdir(), 'vscode-zsh'); @@ -199,7 +230,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - await this.setupPtyProcess(this.shellLaunchConfig, this._ptyOptions); + await this.setupPtyProcess(this.shellLaunchConfig, this._ptyOptions, injection); return undefined; } catch (err) { this._logService.trace('IPty#spawn native exception', err); @@ -249,8 +280,12 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return undefined; } - private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise { - const args = shellLaunchConfig.args || []; + private async setupPtyProcess( + shellLaunchConfig: IShellLaunchConfig, + options: pty.IPtyForkOptions, + shellIntegrationInjection: IShellIntegrationConfigInjection | undefined + ): Promise { + const args = shellIntegrationInjection?.newArgs || shellLaunchConfig.args || []; await this._throttleKillSpawn(); this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options); const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); @@ -364,6 +399,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } this._currentTitle = ptyProcess.process; this._onDidChangeProperty.fire({ type: ProcessPropertyType.Title, value: this._currentTitle }); + this._onDidChangeProperty.fire({ type: ProcessPropertyType.ShellType, value: posixShellTypeMap.get(this.currentTitle) }); } shutdown(immediate: boolean): void { diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts new file mode 100644 index 00000000000..84fb8a7e08d --- /dev/null +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, ok, strictEqual } from 'assert'; +import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; +import { getShellIntegrationInjection, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; + +const enabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { enabled: true, showWelcome: true }; +const disabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { enabled: false, showWelcome: true }; +const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; +const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); + +suite('platform - terminalEnvironment', () => { + suite('getShellIntegrationInjection', () => { + suite('should not enable', () => { + test('when isFeatureTerminal or when no executable is provided', () => { + ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions)); + ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions)); + }); + }); + + suite('pwsh', () => { + const expectedPs1 = process.platform === 'win32' + ? `${repoRoot}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1` + : `${repoRoot}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1`; + suite('should override args', () => { + const enabledExpectedResult = Object.freeze({ + newArgs: [ + '-noexit', + '-command', + `. "${expectedPs1}"` + ] + }); + test('when undefined, []', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions), enabledExpectedResult); + }); + suite('when no logo', () => { + test('array - case insensitive', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-nol'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOL'] }, enabledProcessOptions), enabledExpectedResult); + }); + test('string - case insensitive', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NoLogo' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOLOGO' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-nol' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOL' }, enabledProcessOptions), enabledExpectedResult); + }); + }); + }); + suite('should incorporate login arg', () => { + const enabledExpectedResult = Object.freeze({ + newArgs: [ + '-l', + '-noexit', + '-command', + `. "${expectedPs1}"` + ] + }); + test('when array contains no logo and login', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); + }); + test('when string', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions), enabledExpectedResult); + }); + }); + suite('should not modify args', () => { + test('when shell integration is disabled', () => { + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions), undefined); + }); + test('when using unrecognized arg', () => { + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions), undefined); + }); + test('when using unrecognized arg (string)', () => { + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions), undefined); + }); + }); + }); + + if (process.platform !== 'win32') { + suite('zsh', () => { + suite('should override args', () => { + const expectedDir = /.+\/vscode-zsh/; + const expectedDest = /.+\/vscode-zsh\/.zshrc/; + const expectedSource = /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration.zsh/; + function assertIsEnabled(result: IShellIntegrationConfigInjection) { + strictEqual(Object.keys(result.envMixin!).length, 1); + ok(result.envMixin!['ZDOTDIR']?.match(expectedDir)); + strictEqual(result.filesToCopy?.length, 1); + ok(result.filesToCopy[0].dest.match(expectedDest)); + ok(result.filesToCopy[0].source.match(expectedSource)); + } + test('when undefined, []', () => { + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions); + deepStrictEqual(result1?.newArgs, ['-i']); + assertIsEnabled(result1); + const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions); + deepStrictEqual(result2?.newArgs, ['-i']); + assertIsEnabled(result2); + }); + suite('should incorporate login arg', () => { + test('when array', () => { + const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions); + deepStrictEqual(result?.newArgs, ['-il']); + assertIsEnabled(result); + }); + }); + suite('should not modify args', () => { + test('when shell integration is disabled', () => { + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions), undefined); + }); + test('when using unrecognized arg', () => { + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions), undefined); + }); + }); + }); + }); + suite('bash', () => { + suite('should override args', () => { + test('when undefined, [], empty string', () => { + const enabledExpectedResult = Object.freeze({ + newArgs: [ + '--init-file', + `${repoRoot}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh` + ], + envMixin: {} + }); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions), enabledExpectedResult); + }); + suite('should set login env variable and not modify args', () => { + const enabledExpectedResult = Object.freeze({ + newArgs: [ + '--init-file', + `${repoRoot}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh` + ], + envMixin: { + VSCODE_SHELL_LOGIN: '1' + } + }); + test('when array', () => { + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions), enabledExpectedResult); + }); + }); + suite('should not modify args', () => { + test('when shell integration is disabled', () => { + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions), undefined); + }); + test('when custom array entry', () => { + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions), undefined); + }); + }); + }); + }); + } + }); +}); diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 2fa15866c86..e92c8e8f259 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -55,7 +55,8 @@ export type ColorTransform = export interface ColorDefaults { light: ColorValue | null; dark: ColorValue | null; - hc: ColorValue | null; + hcDark: ColorValue | null; + hcLight: ColorValue | null; } /** @@ -186,8 +187,22 @@ class ColorRegistry implements IColorRegistry { const colorRegistry = new ColorRegistry(); platform.Registry.add(Extensions.ColorContribution, colorRegistry); +function migrateColorDefaults(o: any): null | ColorDefaults { + if (o === null) { + return o; + } + if (typeof o.hcLight === 'undefined') { + if (o.hcDark === null || typeof o.hcDark === 'string') { + o.hcLight = o.hcDark; + } else { + o.hcLight = o.light; + } + } + return o as ColorDefaults; +} + export function registerColor(id: string, defaults: ColorDefaults | null, description: string, needsTransparency?: boolean, deprecationMessage?: string): ColorIdentifier { - return colorRegistry.registerColor(id, defaults, description, needsTransparency, deprecationMessage); + return colorRegistry.registerColor(id, migrateColorDefaults(defaults), description, needsTransparency, deprecationMessage); } export function getColorRegistry(): IColorRegistry { @@ -196,190 +211,190 @@ export function getColorRegistry(): IColorRegistry { // ----- base colors -export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hc: '#FFFFFF' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); -export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hc: '#F48771' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); -export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hc: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); -export const iconForeground = registerColor('icon.foreground', { dark: '#C5C5C5', light: '#424242', hc: '#FFFFFF' }, nls.localize('iconForeground', "The default color for icons in the workbench.")); +export const foreground = registerColor('foreground', { dark: '#CCCCCC', light: '#616161', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('foreground', "Overall foreground color. This color is only used if not overridden by a component.")); +export const errorForeground = registerColor('errorForeground', { dark: '#F48771', light: '#A1260D', hcDark: '#F48771', hcLight: '#B5200D' }, nls.localize('errorForeground', "Overall foreground color for error messages. This color is only used if not overridden by a component.")); +export const descriptionForeground = registerColor('descriptionForeground', { light: '#717171', dark: transparent(foreground, 0.7), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('descriptionForeground', "Foreground color for description text providing additional information, for example for a label.")); +export const iconForeground = registerColor('icon.foreground', { dark: '#C5C5C5', light: '#424242', hcDark: '#FFFFFF', hcLight: '#292929' }, nls.localize('iconForeground', "The default color for icons in the workbench.")); -export const focusBorder = registerColor('focusBorder', { dark: '#007FD4', light: '#0090F1', hc: '#F38518' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); +export const focusBorder = registerColor('focusBorder', { dark: '#007FD4', light: '#0090F1', hcDark: '#F38518', hcLight: '#0F4A85' }, nls.localize('focusBorder', "Overall border color for focused elements. This color is only used if not overridden by a component.")); -export const contrastBorder = registerColor('contrastBorder', { light: null, dark: null, hc: '#6FC3DF' }, nls.localize('contrastBorder', "An extra border around elements to separate them from others for greater contrast.")); -export const activeContrastBorder = registerColor('contrastActiveBorder', { light: null, dark: null, hc: focusBorder }, nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); +export const contrastBorder = registerColor('contrastBorder', { light: null, dark: null, hcDark: '#6FC3DF', hcLight: '#0F4A85' }, nls.localize('contrastBorder', "An extra border around elements to separate them from others for greater contrast.")); +export const activeContrastBorder = registerColor('contrastActiveBorder', { light: null, dark: null, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('activeContrastBorder', "An extra border around active elements to separate them from others for greater contrast.")); -export const selectionBackground = registerColor('selection.background', { light: null, dark: null, hc: null }, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); +export const selectionBackground = registerColor('selection.background', { light: null, dark: null, hcDark: null, hcLight: null }, nls.localize('selectionBackground', "The background color of text selections in the workbench (e.g. for input fields or text areas). Note that this does not apply to selections within the editor.")); // ------ text colors -export const textSeparatorForeground = registerColor('textSeparator.foreground', { light: '#0000002e', dark: '#ffffff2e', hc: Color.black }, nls.localize('textSeparatorForeground', "Color for text separators.")); -export const textLinkForeground = registerColor('textLink.foreground', { light: '#006AB1', dark: '#3794FF', hc: '#3794FF' }, nls.localize('textLinkForeground', "Foreground color for links in text.")); -export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#006AB1', dark: '#3794FF', hc: '#3794FF' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); -export const textPreformatForeground = registerColor('textPreformat.foreground', { light: '#A31515', dark: '#D7BA7D', hc: '#D7BA7D' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); -export const textBlockQuoteBackground = registerColor('textBlockQuote.background', { light: '#7f7f7f1a', dark: '#7f7f7f1a', hc: null }, nls.localize('textBlockQuoteBackground', "Background color for block quotes in text.")); -export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { light: '#007acc80', dark: '#007acc80', hc: Color.white }, nls.localize('textBlockQuoteBorder', "Border color for block quotes in text.")); -export const textCodeBlockBackground = registerColor('textCodeBlock.background', { light: '#dcdcdc66', dark: '#0a0a0a66', hc: Color.black }, nls.localize('textCodeBlockBackground', "Background color for code blocks in text.")); +export const textSeparatorForeground = registerColor('textSeparator.foreground', { light: '#0000002e', dark: '#ffffff2e', hcDark: Color.black, hcLight: '#292929' }, nls.localize('textSeparatorForeground', "Color for text separators.")); +export const textLinkForeground = registerColor('textLink.foreground', { light: '#006AB1', dark: '#3794FF', hcDark: '#3794FF', hcLight: '#0F4A85' }, nls.localize('textLinkForeground', "Foreground color for links in text.")); +export const textLinkActiveForeground = registerColor('textLink.activeForeground', { light: '#006AB1', dark: '#3794FF', hcDark: '#3794FF', hcLight: '#0F4A85' }, nls.localize('textLinkActiveForeground', "Foreground color for links in text when clicked on and on mouse hover.")); +export const textPreformatForeground = registerColor('textPreformat.foreground', { light: '#A31515', dark: '#D7BA7D', hcDark: '#D7BA7D', hcLight: '#292929' }, nls.localize('textPreformatForeground', "Foreground color for preformatted text segments.")); +export const textBlockQuoteBackground = registerColor('textBlockQuote.background', { light: '#7f7f7f1a', dark: '#7f7f7f1a', hcDark: null, hcLight: '#F2F2F2' }, nls.localize('textBlockQuoteBackground', "Background color for block quotes in text.")); +export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { light: '#007acc80', dark: '#007acc80', hcDark: Color.white, hcLight: '#292929' }, nls.localize('textBlockQuoteBorder', "Border color for block quotes in text.")); +export const textCodeBlockBackground = registerColor('textCodeBlock.background', { light: '#dcdcdc66', dark: '#0a0a0a66', hcDark: Color.black, hcLight: '#F2F2F2' }, nls.localize('textCodeBlockBackground', "Background color for code blocks in text.")); // ----- widgets -export const widgetShadow = registerColor('widget.shadow', { dark: transparent(Color.black, .36), light: transparent(Color.black, .16), hc: null }, nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.')); +export const widgetShadow = registerColor('widget.shadow', { dark: transparent(Color.black, .36), light: transparent(Color.black, .16), hcDark: null, hcLight: null }, nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.')); -export const inputBackground = registerColor('input.background', { dark: '#3C3C3C', light: Color.white, hc: Color.black }, nls.localize('inputBoxBackground', "Input box background.")); -export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground.")); -export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border.")); -export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC00', light: '#007ACC00', hc: contrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); -export const inputActiveOptionHoverBackground = registerColor('inputOption.hoverBackground', { dark: '#5a5d5e80', light: '#b8b8b850', hc: null }, nls.localize('inputOption.hoverBackground', "Background color of activated options in input fields.")); -export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hc: Color.transparent }, nls.localize('inputOption.activeBackground', "Background hover color of options in input fields.")); -export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hc: null }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); -export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); +export const inputBackground = registerColor('input.background', { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, nls.localize('inputBoxBackground', "Input box background.")); +export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('inputBoxForeground', "Input box foreground.")); +export const inputBorder = registerColor('input.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputBoxBorder', "Input box border.")); +export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC00', light: '#007ACC00', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); +export const inputActiveOptionHoverBackground = registerColor('inputOption.hoverBackground', { dark: '#5a5d5e80', light: '#b8b8b850', hcDark: null, hcLight: null }, nls.localize('inputOption.hoverBackground', "Background color of activated options in input fields.")); +export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hcDark: Color.transparent, hcLight: Color.transparent }, nls.localize('inputOption.activeBackground', "Background hover color of options in input fields.")); +export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hcDark: null, hcLight: null }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); +export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); -export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); -export const inputValidationInfoForeground = registerColor('inputValidation.infoForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationInfoForeground', "Input validation foreground color for information severity.")); -export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); -export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hc: Color.black }, nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); -export const inputValidationWarningForeground = registerColor('inputValidation.warningForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationWarningForeground', "Input validation foreground color for warning severity.")); -export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', { dark: '#B89500', light: '#B89500', hc: contrastBorder }, nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); -export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', { dark: '#5A1D1D', light: '#F2DEDE', hc: Color.black }, nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); -export const inputValidationErrorForeground = registerColor('inputValidation.errorForeground', { dark: null, light: null, hc: null }, nls.localize('inputValidationErrorForeground', "Input validation foreground color for error severity.")); -export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', { dark: '#BE1100', light: '#BE1100', hc: contrastBorder }, nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); +export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); +export const inputValidationInfoForeground = registerColor('inputValidation.infoForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationInfoForeground', "Input validation foreground color for information severity.")); +export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity.")); +export const inputValidationWarningBackground = registerColor('inputValidation.warningBackground', { dark: '#352A05', light: '#F6F5D2', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationWarningBackground', "Input validation background color for warning severity.")); +export const inputValidationWarningForeground = registerColor('inputValidation.warningForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationWarningForeground', "Input validation foreground color for warning severity.")); +export const inputValidationWarningBorder = registerColor('inputValidation.warningBorder', { dark: '#B89500', light: '#B89500', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationWarningBorder', "Input validation border color for warning severity.")); +export const inputValidationErrorBackground = registerColor('inputValidation.errorBackground', { dark: '#5A1D1D', light: '#F2DEDE', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationErrorBackground', "Input validation background color for error severity.")); +export const inputValidationErrorForeground = registerColor('inputValidation.errorForeground', { dark: null, light: null, hcDark: null, hcLight: foreground }, nls.localize('inputValidationErrorForeground', "Input validation foreground color for error severity.")); +export const inputValidationErrorBorder = registerColor('inputValidation.errorBorder', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputValidationErrorBorder', "Input validation border color for error severity.")); -export const selectBackground = registerColor('dropdown.background', { dark: '#3C3C3C', light: Color.white, hc: Color.black }, nls.localize('dropdownBackground', "Dropdown background.")); -export const selectListBackground = registerColor('dropdown.listBackground', { dark: null, light: null, hc: Color.black }, nls.localize('dropdownListBackground', "Dropdown list background.")); -export const selectForeground = registerColor('dropdown.foreground', { dark: '#F0F0F0', light: null, hc: Color.white }, nls.localize('dropdownForeground', "Dropdown foreground.")); -export const selectBorder = registerColor('dropdown.border', { dark: selectBackground, light: '#CECECE', hc: contrastBorder }, nls.localize('dropdownBorder', "Dropdown border.")); +export const selectBackground = registerColor('dropdown.background', { dark: '#3C3C3C', light: Color.white, hcDark: Color.black, hcLight: Color.white }, nls.localize('dropdownBackground', "Dropdown background.")); +export const selectListBackground = registerColor('dropdown.listBackground', { dark: null, light: null, hcDark: Color.black, hcLight: Color.white }, nls.localize('dropdownListBackground', "Dropdown list background.")); +export const selectForeground = registerColor('dropdown.foreground', { dark: '#F0F0F0', light: null, hcDark: Color.white, hcLight: foreground }, nls.localize('dropdownForeground', "Dropdown foreground.")); +export const selectBorder = registerColor('dropdown.border', { dark: selectBackground, light: '#CECECE', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('dropdownBorder', "Dropdown border.")); -export const simpleCheckboxBackground = registerColor('checkbox.background', { dark: selectBackground, light: selectBackground, hc: selectBackground }, nls.localize('checkbox.background', "Background color of checkbox widget.")); -export const simpleCheckboxForeground = registerColor('checkbox.foreground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); -export const simpleCheckboxBorder = registerColor('checkbox.border', { dark: selectBorder, light: selectBorder, hc: selectBorder }, nls.localize('checkbox.border', "Border color of checkbox widget.")); +export const checkboxBackground = registerColor('checkbox.background', { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, nls.localize('checkbox.background', "Background color of checkbox widget.")); +export const checkboxForeground = registerColor('checkbox.foreground', { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, nls.localize('checkbox.foreground', "Foreground color of checkbox widget.")); +export const checkboxBorder = registerColor('checkbox.border', { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, nls.localize('checkbox.border', "Border color of checkbox widget.")); -export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); -export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hc: null }, nls.localize('buttonBackground', "Button background color.")); -export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hc: null }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); -export const buttonBorder = registerColor('button.border', { dark: contrastBorder, light: contrastBorder, hc: contrastBorder }, nls.localize('buttonBorder', "Button border color.")); +export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); +export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hcDark: null, hcLight: '#0F4A85' }, nls.localize('buttonBackground', "Button background color.")); +export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); +export const buttonBorder = registerColor('button.border', { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('buttonBorder', "Button border color.")); -export const buttonSecondaryForeground = registerColor('button.secondaryForeground', { dark: Color.white, light: Color.white, hc: Color.white }, nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); -export const buttonSecondaryBackground = registerColor('button.secondaryBackground', { dark: '#3A3D41', light: '#5F6A79', hc: null }, nls.localize('buttonSecondaryBackground', "Secondary button background color.")); -export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hc: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); +export const buttonSecondaryForeground = registerColor('button.secondaryForeground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: foreground }, nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); +export const buttonSecondaryBackground = registerColor('button.secondaryBackground', { dark: '#3A3D41', light: '#5F6A79', hcDark: null, hcLight: Color.white }, nls.localize('buttonSecondaryBackground', "Secondary button background color.")); +export const buttonSecondaryHoverBackground = registerColor('button.secondaryHoverBackground', { dark: lighten(buttonSecondaryBackground, 0.2), light: darken(buttonSecondaryBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonSecondaryHoverBackground', "Secondary button background color when hovering.")); -export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hc: Color.black }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); -export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hc: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); +export const badgeBackground = registerColor('badge.background', { dark: '#4D4D4D', light: '#C4C4C4', hcDark: Color.black, hcLight: '#007ACC' }, nls.localize('badgeBackground', "Badge background color. Badges are small information labels, e.g. for search results count.")); +export const badgeForeground = registerColor('badge.foreground', { dark: Color.white, light: '#333', hcDark: Color.white, hcLight: Color.white }, nls.localize('badgeForeground', "Badge foreground color. Badges are small information labels, e.g. for search results count.")); -export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hc: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); -export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hc: transparent(contrastBorder, 0.6) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); -export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hc: transparent(contrastBorder, 0.8) }, nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering.")); -export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hc: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on.")); +export const scrollbarShadow = registerColor('scrollbar.shadow', { dark: '#000000', light: '#DDDDDD', hcDark: null, hcLight: null }, nls.localize('scrollbarShadow', "Scrollbar shadow to indicate that the view is scrolled.")); +export const scrollbarSliderBackground = registerColor('scrollbarSlider.background', { dark: Color.fromHex('#797979').transparent(0.4), light: Color.fromHex('#646464').transparent(0.4), hcDark: transparent(contrastBorder, 0.6), hcLight: transparent(contrastBorder, 0.6) }, nls.localize('scrollbarSliderBackground', "Scrollbar slider background color.")); +export const scrollbarSliderHoverBackground = registerColor('scrollbarSlider.hoverBackground', { dark: Color.fromHex('#646464').transparent(0.7), light: Color.fromHex('#646464').transparent(0.7), hcDark: transparent(contrastBorder, 0.8), hcLight: transparent(contrastBorder, 0.8) }, nls.localize('scrollbarSliderHoverBackground', "Scrollbar slider background color when hovering.")); +export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.activeBackground', { dark: Color.fromHex('#BFBFBF').transparent(0.4), light: Color.fromHex('#000000').transparent(0.6), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('scrollbarSliderActiveBackground', "Scrollbar slider background color when clicked on.")); -export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); +export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations.")); -export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hc: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hc: null }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); -export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hc: Color.fromHex('#E47777').transparent(0.8) }, nls.localize('errorBorder', 'Border color of error boxes in the editor.')); +export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: '#B5200D' }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.')); +export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hcDark: Color.fromHex('#E47777').transparent(0.8), hcLight: '#B5200D' }, nls.localize('errorBorder', 'Border color of error boxes in the editor.')); -export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hc: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#BF8803', hc: null }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); -export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hc: Color.fromHex('#FFCC00').transparent(0.8) }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.')); +export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: '#895503' }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.')); +export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hcDark: Color.fromHex('#FFCC00').transparent(0.8), hcLight: '#' }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.')); -export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hc: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#3794FF', light: '#1a85ff', hc: '#3794FF' }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); -export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hc: Color.fromHex('#3794FF').transparent(0.8) }, nls.localize('infoBorder', 'Border color of info boxes in the editor.')); +export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#3794FF', light: '#1a85ff', hcDark: '#3794FF', hcLight: '#1a85ff' }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.')); +export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hcDark: Color.fromHex('#3794FF').transparent(0.8), hcLight: '#292929' }, nls.localize('infoBorder', 'Border color of info boxes in the editor.')); -export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hc: null }, nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.')); -export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hc: Color.fromHex('#eeeeee').transparent(0.8) }, nls.localize('hintBorder', 'Border color of hint boxes in the editor.')); +export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hcDark: null, hcLight: null }, nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.')); +export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hcDark: Color.fromHex('#eeeeee').transparent(0.8), hcLight: '#292929' }, nls.localize('hintBorder', 'Border color of hint boxes in the editor.')); -export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: focusBorder, light: focusBorder, hc: focusBorder }, nls.localize('sashActiveBorder', "Border color of active sashes.")); +export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, nls.localize('sashActiveBorder', "Border color of active sashes.")); /** * Editor background color. * Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254 * we are *not* using the color white (or #ffffff, rgba(255,255,255)) but something very close to white. */ -export const editorBackground = registerColor('editor.background', { light: '#fffffe', dark: '#1E1E1E', hc: Color.black }, nls.localize('editorBackground', "Editor background color.")); +export const editorBackground = registerColor('editor.background', { light: '#fffffe', dark: '#1E1E1E', hcDark: Color.black, hcLight: Color.white }, nls.localize('editorBackground', "Editor background color.")); /** * Editor foreground color. */ -export const editorForeground = registerColor('editor.foreground', { light: '#333333', dark: '#BBBBBB', hc: Color.white }, nls.localize('editorForeground', "Editor default foreground color.")); +export const editorForeground = registerColor('editor.foreground', { light: '#333333', dark: '#BBBBBB', hcDark: Color.white, hcLight: foreground }, nls.localize('editorForeground', "Editor default foreground color.")); /** * Editor widgets */ -export const editorWidgetBackground = registerColor('editorWidget.background', { dark: '#252526', light: '#F3F3F3', hc: '#0C141F' }, nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); -export const editorWidgetForeground = registerColor('editorWidget.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); +export const editorWidgetBackground = registerColor('editorWidget.background', { dark: '#252526', light: '#F3F3F3', hcDark: '#0C141F', hcLight: Color.white }, nls.localize('editorWidgetBackground', 'Background color of editor widgets, such as find/replace.')); +export const editorWidgetForeground = registerColor('editorWidget.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('editorWidgetForeground', 'Foreground color of editor widgets, such as find/replace.')); -export const editorWidgetBorder = registerColor('editorWidget.border', { dark: '#454545', light: '#C8C8C8', hc: contrastBorder }, nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); +export const editorWidgetBorder = registerColor('editorWidget.border', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorWidgetBorder', 'Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.')); -export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', { light: null, dark: null, hc: null }, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); +export const editorWidgetResizeBorder = registerColor('editorWidget.resizeBorder', { light: null, dark: null, hcDark: null, hcLight: null }, nls.localize('editorWidgetResizeBorder', "Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.")); /** * Quick pick widget */ -export const quickInputBackground = registerColor('quickInput.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); -export const quickInputForeground = registerColor('quickInput.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, hc: editorWidgetForeground }, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); -export const quickInputTitleBackground = registerColor('quickInputTitle.background', { dark: new Color(new RGBA(255, 255, 255, 0.105)), light: new Color(new RGBA(0, 0, 0, 0.06)), hc: '#000000' }, nls.localize('pickerTitleBackground', "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.")); -export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#0066BF', hc: Color.white }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); -export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hc: Color.white }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); +export const quickInputBackground = registerColor('quickInput.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('pickerBackground', "Quick picker background color. The quick picker widget is the container for pickers like the command palette.")); +export const quickInputForeground = registerColor('quickInput.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, nls.localize('pickerForeground', "Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.")); +export const quickInputTitleBackground = registerColor('quickInputTitle.background', { dark: new Color(new RGBA(255, 255, 255, 0.105)), light: new Color(new RGBA(0, 0, 0, 0.06)), hcDark: '#000000', hcLight: Color.white }, nls.localize('pickerTitleBackground', "Quick picker title background color. The quick picker widget is the container for pickers like the command palette.")); +export const pickerGroupForeground = registerColor('pickerGroup.foreground', { dark: '#3794FF', light: '#0066BF', hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('pickerGroupForeground', "Quick picker color for grouping labels.")); +export const pickerGroupBorder = registerColor('pickerGroup.border', { dark: '#3F3F46', light: '#CCCEDB', hcDark: Color.white, hcLight: '#0F4A85' }, nls.localize('pickerGroupBorder', "Quick picker color for grouping borders.")); /** * Keybinding label */ -export const keybindingLabelBackground = registerColor('keybindingLabel.background', { dark: new Color(new RGBA(128, 128, 128, 0.17)), light: new Color(new RGBA(221, 221, 221, 0.4)), hc: Color.transparent }, nls.localize('keybindingLabelBackground', "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelForeground = registerColor('keybindingLabel.foreground', { dark: Color.fromHex('#CCCCCC'), light: Color.fromHex('#555555'), hc: Color.white }, nls.localize('keybindingLabelForeground', "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelBorder = registerColor('keybindingLabel.border', { dark: new Color(new RGBA(51, 51, 51, 0.6)), light: new Color(new RGBA(204, 204, 204, 0.4)), hc: new Color(new RGBA(111, 195, 223)) }, nls.localize('keybindingLabelBorder', "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.")); -export const keybindingLabelBottomBorder = registerColor('keybindingLabel.bottomBorder', { dark: new Color(new RGBA(68, 68, 68, 0.6)), light: new Color(new RGBA(187, 187, 187, 0.4)), hc: new Color(new RGBA(111, 195, 223)) }, nls.localize('keybindingLabelBottomBorder', "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.")); +export const keybindingLabelBackground = registerColor('keybindingLabel.background', { dark: new Color(new RGBA(128, 128, 128, 0.17)), light: new Color(new RGBA(221, 221, 221, 0.4)), hcDark: Color.transparent, hcLight: Color.transparent }, nls.localize('keybindingLabelBackground', "Keybinding label background color. The keybinding label is used to represent a keyboard shortcut.")); +export const keybindingLabelForeground = registerColor('keybindingLabel.foreground', { dark: Color.fromHex('#CCCCCC'), light: Color.fromHex('#555555'), hcDark: Color.white, hcLight: foreground }, nls.localize('keybindingLabelForeground', "Keybinding label foreground color. The keybinding label is used to represent a keyboard shortcut.")); +export const keybindingLabelBorder = registerColor('keybindingLabel.border', { dark: new Color(new RGBA(51, 51, 51, 0.6)), light: new Color(new RGBA(204, 204, 204, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: contrastBorder }, nls.localize('keybindingLabelBorder', "Keybinding label border color. The keybinding label is used to represent a keyboard shortcut.")); +export const keybindingLabelBottomBorder = registerColor('keybindingLabel.bottomBorder', { dark: new Color(new RGBA(68, 68, 68, 0.6)), light: new Color(new RGBA(187, 187, 187, 0.4)), hcDark: new Color(new RGBA(111, 195, 223)), hcLight: foreground }, nls.localize('keybindingLabelBottomBorder', "Keybinding label border bottom color. The keybinding label is used to represent a keyboard shortcut.")); /** * Editor selection colors. */ -export const editorSelectionBackground = registerColor('editor.selectionBackground', { light: '#ADD6FF', dark: '#264F78', hc: '#f3f518' }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); -export const editorSelectionForeground = registerColor('editor.selectionForeground', { light: null, dark: null, hc: '#000000' }, nls.localize('editorSelectionForeground', "Color of the selected text for high contrast.")); -export const editorInactiveSelection = registerColor('editor.inactiveSelectionBackground', { light: transparent(editorSelectionBackground, 0.5), dark: transparent(editorSelectionBackground, 0.5), hc: transparent(editorSelectionBackground, 0.5) }, nls.localize('editorInactiveSelection', "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorSelectionHighlight = registerColor('editor.selectionHighlightBackground', { light: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), dark: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), hc: null }, nls.localize('editorSelectionHighlight', 'Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorSelectionHighlightBorder = registerColor('editor.selectionHighlightBorder', { light: null, dark: null, hc: activeContrastBorder }, nls.localize('editorSelectionHighlightBorder', "Border color for regions with the same content as the selection.")); +export const editorSelectionBackground = registerColor('editor.selectionBackground', { light: '#ADD6FF', dark: '#264F78', hcDark: '#f3f518', hcLight: '#0F4A85' }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); +export const editorSelectionForeground = registerColor('editor.selectionForeground', { light: null, dark: null, hcDark: '#000000', hcLight: Color.white }, nls.localize('editorSelectionForeground', "Color of the selected text for high contrast.")); +export const editorInactiveSelection = registerColor('editor.inactiveSelectionBackground', { light: transparent(editorSelectionBackground, 0.5), dark: transparent(editorSelectionBackground, 0.5), hcDark: transparent(editorSelectionBackground, 0.7), hcLight: transparent(editorSelectionBackground, 0.5) }, nls.localize('editorInactiveSelection', "Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorSelectionHighlight = registerColor('editor.selectionHighlightBackground', { light: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), dark: lessProminent(editorSelectionBackground, editorBackground, 0.3, 0.6), hcDark: null, hcLight: null }, nls.localize('editorSelectionHighlight', 'Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorSelectionHighlightBorder = registerColor('editor.selectionHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('editorSelectionHighlightBorder', "Border color for regions with the same content as the selection.")); /** * Editor find match colors. */ -export const editorFindMatch = registerColor('editor.findMatchBackground', { light: '#A8AC94', dark: '#515C6A', hc: null }, nls.localize('editorFindMatch', "Color of the current search match.")); -export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', { light: '#EA5C0055', dark: '#EA5C0055', hc: null }, nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', { dark: '#3a3d4166', light: '#b4b4b44d', hc: null }, nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); -export const editorFindMatchBorder = registerColor('editor.findMatchBorder', { light: null, dark: null, hc: activeContrastBorder }, nls.localize('editorFindMatchBorder', "Border color of the current search match.")); -export const editorFindMatchHighlightBorder = registerColor('editor.findMatchHighlightBorder', { light: null, dark: null, hc: activeContrastBorder }, nls.localize('findMatchHighlightBorder', "Border color of the other search matches.")); -export const editorFindRangeHighlightBorder = registerColor('editor.findRangeHighlightBorder', { dark: null, light: null, hc: transparent(activeContrastBorder, 0.4) }, nls.localize('findRangeHighlightBorder', "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorFindMatch = registerColor('editor.findMatchBackground', { light: '#A8AC94', dark: '#515C6A', hcDark: null, hcLight: null }, nls.localize('editorFindMatch', "Color of the current search match.")); +export const editorFindMatchHighlight = registerColor('editor.findMatchHighlightBackground', { light: '#EA5C0055', dark: '#EA5C0055', hcDark: null, hcLight: null }, nls.localize('findMatchHighlight', "Color of the other search matches. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorFindRangeHighlight = registerColor('editor.findRangeHighlightBackground', { dark: '#3a3d4166', light: '#b4b4b44d', hcDark: null, hcLight: null }, nls.localize('findRangeHighlight', "Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); +export const editorFindMatchBorder = registerColor('editor.findMatchBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('editorFindMatchBorder', "Border color of the current search match.")); +export const editorFindMatchHighlightBorder = registerColor('editor.findMatchHighlightBorder', { light: null, dark: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('findMatchHighlightBorder', "Border color of the other search matches.")); +export const editorFindRangeHighlightBorder = registerColor('editor.findRangeHighlightBorder', { dark: null, light: null, hcDark: transparent(activeContrastBorder, 0.4), hcLight: transparent(activeContrastBorder, 0.4) }, nls.localize('findRangeHighlightBorder', "Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations."), true); /** * Search Editor query match colors. * * Distinct from normal editor find match to allow for better differentiation */ -export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', { light: transparent(editorFindMatchHighlight, 0.66), dark: transparent(editorFindMatchHighlight, 0.66), hc: editorFindMatchHighlight }, nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); -export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', { light: transparent(editorFindMatchHighlightBorder, 0.66), dark: transparent(editorFindMatchHighlightBorder, 0.66), hc: editorFindMatchHighlightBorder }, nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); +export const searchEditorFindMatch = registerColor('searchEditor.findMatchBackground', { light: transparent(editorFindMatchHighlight, 0.66), dark: transparent(editorFindMatchHighlight, 0.66), hcDark: editorFindMatchHighlight, hcLight: editorFindMatchHighlight }, nls.localize('searchEditor.queryMatch', "Color of the Search Editor query matches.")); +export const searchEditorFindMatchBorder = registerColor('searchEditor.findMatchBorder', { light: transparent(editorFindMatchHighlightBorder, 0.66), dark: transparent(editorFindMatchHighlightBorder, 0.66), hcDark: editorFindMatchHighlightBorder, hcLight: editorFindMatchHighlightBorder }, nls.localize('searchEditor.editorFindMatchBorder', "Border color of the Search Editor query matches.")); /** * Editor hover */ -export const editorHoverHighlight = registerColor('editor.hoverHighlightBackground', { light: '#ADD6FF26', dark: '#264f7840', hc: '#ADD6FF26' }, nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); -export const editorHoverBackground = registerColor('editorHoverWidget.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('hoverBackground', 'Background color of the editor hover.')); -export const editorHoverForeground = registerColor('editorHoverWidget.foreground', { light: editorWidgetForeground, dark: editorWidgetForeground, hc: editorWidgetForeground }, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); -export const editorHoverBorder = registerColor('editorHoverWidget.border', { light: editorWidgetBorder, dark: editorWidgetBorder, hc: editorWidgetBorder }, nls.localize('hoverBorder', 'Border color of the editor hover.')); -export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', { dark: lighten(editorHoverBackground, 0.2), light: darken(editorHoverBackground, 0.05), hc: editorWidgetBackground }, nls.localize('statusBarBackground', "Background color of the editor hover status bar.")); +export const editorHoverHighlight = registerColor('editor.hoverHighlightBackground', { light: '#ADD6FF26', dark: '#264f7840', hcDark: '#ADD6FF26', hcLight: null }, nls.localize('hoverHighlight', 'Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.'), true); +export const editorHoverBackground = registerColor('editorHoverWidget.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('hoverBackground', 'Background color of the editor hover.')); +export const editorHoverForeground = registerColor('editorHoverWidget.foreground', { light: editorWidgetForeground, dark: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, nls.localize('hoverForeground', 'Foreground color of the editor hover.')); +export const editorHoverBorder = registerColor('editorHoverWidget.border', { light: editorWidgetBorder, dark: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, nls.localize('hoverBorder', 'Border color of the editor hover.')); +export const editorHoverStatusBarBackground = registerColor('editorHoverWidget.statusBarBackground', { dark: lighten(editorHoverBackground, 0.2), light: darken(editorHoverBackground, 0.05), hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('statusBarBackground', "Background color of the editor hover status bar.")); /** * Editor link colors */ -export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hc: Color.cyan }, nls.localize('activeLinkForeground', 'Color of active links.')); +export const editorActiveLinkForeground = registerColor('editorLink.activeForeground', { dark: '#4E94CE', light: Color.blue, hcDark: Color.cyan, hcLight: '#292929' }, nls.localize('activeLinkForeground', 'Color of active links.')); /** * Inline hints */ -export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: transparent(badgeForeground, .8), light: transparent(badgeForeground, .8), hc: badgeForeground }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); -export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: transparent(badgeBackground, .6), light: transparent(badgeBackground, .3), hc: badgeBackground }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); -export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hc: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); -export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hc: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); -export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hc: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); -export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hc: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); +export const editorInlayHintForeground = registerColor('editorInlayHint.foreground', { dark: transparent(badgeForeground, .8), light: transparent(badgeForeground, .8), hcDark: badgeForeground, hcLight: badgeForeground }, nls.localize('editorInlayHintForeground', 'Foreground color of inline hints')); +export const editorInlayHintBackground = registerColor('editorInlayHint.background', { dark: transparent(badgeBackground, .6), light: transparent(badgeBackground, .3), hcDark: badgeBackground, hcLight: badgeBackground }, nls.localize('editorInlayHintBackground', 'Background color of inline hints')); +export const editorInlayHintTypeForeground = registerColor('editorInlayHint.typeForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundTypes', 'Foreground color of inline hints for types')); +export const editorInlayHintTypeBackground = registerColor('editorInlayHint.typeBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundTypes', 'Background color of inline hints for types')); +export const editorInlayHintParameterForeground = registerColor('editorInlayHint.parameterForeground', { dark: editorInlayHintForeground, light: editorInlayHintForeground, hcDark: editorInlayHintForeground, hcLight: editorInlayHintForeground }, nls.localize('editorInlayHintForegroundParameter', 'Foreground color of inline hints for parameters')); +export const editorInlayHintParameterBackground = registerColor('editorInlayHint.parameterBackground', { dark: editorInlayHintBackground, light: editorInlayHintBackground, hcDark: editorInlayHintBackground, hcLight: editorInlayHintBackground }, nls.localize('editorInlayHintBackgroundParameter', 'Background color of inline hints for parameters')); /** * Editor lighbulb icon colors */ -export const editorLightBulbForeground = registerColor('editorLightBulb.foreground', { dark: '#FFCC00', light: '#DDB100', hc: '#FFCC00' }, nls.localize('editorLightBulbForeground', "The color used for the lightbulb actions icon.")); -export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAutoFix.foreground', { dark: '#75BEFF', light: '#007ACC', hc: '#75BEFF' }, nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); +export const editorLightBulbForeground = registerColor('editorLightBulb.foreground', { dark: '#FFCC00', light: '#DDB100', hcDark: '#FFCC00', hcLight: '#007ACC' }, nls.localize('editorLightBulbForeground', "The color used for the lightbulb actions icon.")); +export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAutoFix.foreground', { dark: '#75BEFF', light: '#007ACC', hcDark: '#75BEFF', hcLight: '#007ACC' }, nls.localize('editorLightBulbAutoFixForeground', "The color used for the lightbulb auto fix actions icon.")); /** * Diff Editor Colors @@ -387,98 +402,98 @@ export const editorLightBulbAutoFixForeground = registerColor('editorLightBulbAu export const defaultInsertColor = new Color(new RGBA(155, 185, 85, 0.2)); export const defaultRemoveColor = new Color(new RGBA(255, 0, 0, 0.2)); -export const diffInserted = registerColor('diffEditor.insertedTextBackground', { dark: defaultInsertColor, light: defaultInsertColor, hc: null }, nls.localize('diffEditorInserted', 'Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffRemoved = registerColor('diffEditor.removedTextBackground', { dark: defaultRemoveColor, light: defaultRemoveColor, hc: null }, nls.localize('diffEditorRemoved', 'Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); +export const diffInserted = registerColor('diffEditor.insertedTextBackground', { dark: defaultInsertColor, light: defaultInsertColor, hcDark: null, hcLight: null }, nls.localize('diffEditorInserted', 'Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); +export const diffRemoved = registerColor('diffEditor.removedTextBackground', { dark: defaultRemoveColor, light: defaultRemoveColor, hcDark: null, hcLight: null }, nls.localize('diffEditorRemoved', 'Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffInsertedLine = registerColor('diffEditor.insertedLineBackground', { dark: null, light: null, hc: null }, nls.localize('diffEditorInsertedLines', 'Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', { dark: null, light: null, hc: null }, nls.localize('diffEditorRemovedLines', 'Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); +export const diffInsertedLine = registerColor('diffEditor.insertedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorInsertedLines', 'Background color for lines that got inserted. The color must not be opaque so as not to hide underlying decorations.'), true); +export const diffRemovedLine = registerColor('diffEditor.removedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorRemovedLines', 'Background color for lines that got removed. The color must not be opaque so as not to hide underlying decorations.'), true); -export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', { dark: null, light: null, hc: null }, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); -export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', { dark: null, light: null, hc: null }, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); +export const diffInsertedLineGutter = registerColor('diffEditorGutter.insertedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorInsertedLineGutter', 'Background color for the margin where lines got inserted.')); +export const diffRemovedLineGutter = registerColor('diffEditorGutter.removedLineBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorRemovedLineGutter', 'Background color for the margin where lines got removed.')); -export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', { dark: null, light: null, hc: null }, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); -export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', { dark: null, light: null, hc: null }, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); +export const diffOverviewRulerInserted = registerColor('diffEditorOverview.insertedForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorOverviewInserted', 'Diff overview ruler foreground for inserted content.')); +export const diffOverviewRulerRemoved = registerColor('diffEditorOverview.removedForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('diffEditorOverviewRemoved', 'Diff overview ruler foreground for removed content.')); -export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hc: '#33ff2eff' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); -export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hc: '#FF008F' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); +export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder', { dark: null, light: null, hcDark: '#33ff2eff', hcLight: '#374E06' }, nls.localize('diffEditorInsertedOutline', 'Outline color for the text that got inserted.')); +export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hcDark: '#FF008F', hcLight: '#AD0707' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.')); -export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.')); -export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hc: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.")); +export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.')); +export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hcDark: null, hcLight: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.")); /** * List and tree colors */ -export const listFocusBackground = registerColor('list.focusBackground', { dark: null, light: null, hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listFocusOutline = registerColor('list.focusOutline', { dark: focusBorder, light: focusBorder, hc: activeContrastBorder }, nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', { dark: null, light: null, hc: null }, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: null, light: null, hc: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', { dark: null, light: null, hc: null }, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); -export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hc: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); -export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hc: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); -export const listDropBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); -export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#18A3FF', light: '#0066BF', hc: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); -export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', { dark: listHighlightForeground, light: ifDefinedThenElse(listActiveSelectionBackground, listHighlightForeground, '#9DDDFF'), hc: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); -export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hc: '#B89500' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); -export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hc: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); -export const listWarningForeground = registerColor('list.warningForeground', { dark: '#CCA700', light: '#855F00', hc: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); -export const listFilterWidgetBackground = registerColor('listFilterWidget.background', { light: '#efc1ad', dark: '#653723', hc: Color.black }, nls.localize('listFilterWidgetBackground', 'Background color of the type filter widget in lists and trees.')); -export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', { dark: Color.transparent, light: Color.transparent, hc: '#f38518' }, nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); -export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', { dark: '#BE1100', light: '#BE1100', hc: contrastBorder }, nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); -export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hc: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); -export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hc: contrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); -export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hc: '#a9a9a9' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); -export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', { dark: '#CCCCCC20', light: '#61616120', hc: null }, nls.localize('tableColumnsBorder', "Table border color between columns.")); -export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hc: null }, nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); -export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', { dark: '#8C8C8C', light: '#8E8E90', hc: '#A7A8A9' }, nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized. ")); +export const listFocusBackground = registerColor('list.focusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listFocusOutline = registerColor('list.focusOutline', { dark: focusBorder, light: focusBorder, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('listFocusOutline', "List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hcDark: null, hcLight: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionIconForeground = registerColor('list.activeSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listActiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveSelectionIconForeground = registerColor('list.inactiveSelectionIconForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveSelectionIconForeground', "List/Tree icon foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveFocusBackground = registerColor('list.inactiveFocusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusBackground', "List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listInactiveFocusOutline = registerColor('list.inactiveFocusOutline', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listInactiveFocusOutline', "List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); +export const listHoverBackground = registerColor('list.hoverBackground', { dark: '#2A2D2E', light: '#F0F0F0', hcDark: null, hcLight: null }, nls.localize('listHoverBackground', "List/Tree background when hovering over items using the mouse.")); +export const listHoverForeground = registerColor('list.hoverForeground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('listHoverForeground', "List/Tree foreground when hovering over items using the mouse.")); +export const listDropBackground = registerColor('list.dropBackground', { dark: '#062F4A', light: '#D6EBFF', hcDark: null, hcLight: null }, nls.localize('listDropBackground', "List/Tree drag and drop background when moving items around using the mouse.")); +export const listHighlightForeground = registerColor('list.highlightForeground', { dark: '#18A3FF', light: '#0066BF', hcDark: focusBorder, hcLight: focusBorder }, nls.localize('highlight', 'List/Tree foreground color of the match highlights when searching inside the list/tree.')); +export const listFocusHighlightForeground = registerColor('list.focusHighlightForeground', { dark: listHighlightForeground, light: ifDefinedThenElse(listActiveSelectionBackground, listHighlightForeground, '#9DDDFF'), hcDark: listHighlightForeground, hcLight: listHighlightForeground }, nls.localize('listFocusHighlightForeground', 'List/Tree foreground color of the match highlights on actively focused items when searching inside the list/tree.')); +export const listInvalidItemForeground = registerColor('list.invalidItemForeground', { dark: '#B89500', light: '#B89500', hcDark: '#B89500', hcLight: '#B5200D' }, nls.localize('invalidItemForeground', 'List/Tree foreground color for invalid items, for example an unresolved root in explorer.')); +export const listErrorForeground = registerColor('list.errorForeground', { dark: '#F88070', light: '#B01011', hcDark: null, hcLight: null }, nls.localize('listErrorForeground', 'Foreground color of list items containing errors.')); +export const listWarningForeground = registerColor('list.warningForeground', { dark: '#CCA700', light: '#855F00', hcDark: null, hcLight: null }, nls.localize('listWarningForeground', 'Foreground color of list items containing warnings.')); +export const listFilterWidgetBackground = registerColor('listFilterWidget.background', { light: '#efc1ad', dark: '#653723', hcDark: Color.black, hcLight: Color.white }, nls.localize('listFilterWidgetBackground', 'Background color of the type filter widget in lists and trees.')); +export const listFilterWidgetOutline = registerColor('listFilterWidget.outline', { dark: Color.transparent, light: Color.transparent, hcDark: '#f38518', hcLight: '#007ACC' }, nls.localize('listFilterWidgetOutline', 'Outline color of the type filter widget in lists and trees.')); +export const listFilterWidgetNoMatchesOutline = registerColor('listFilterWidget.noMatchesOutline', { dark: '#BE1100', light: '#BE1100', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterWidgetNoMatchesOutline', 'Outline color of the type filter widget in lists and trees, when there are no matches.')); +export const listFilterMatchHighlight = registerColor('list.filterMatchBackground', { dark: editorFindMatchHighlight, light: editorFindMatchHighlight, hcDark: null, hcLight: null }, nls.localize('listFilterMatchHighlight', 'Background color of the filtered match.')); +export const listFilterMatchHighlightBorder = registerColor('list.filterMatchBorder', { dark: editorFindMatchHighlightBorder, light: editorFindMatchHighlightBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('listFilterMatchHighlightBorder', 'Border color of the filtered match.')); +export const treeIndentGuidesStroke = registerColor('tree.indentGuidesStroke', { dark: '#585858', light: '#a9a9a9', hcDark: '#a9a9a9', hcLight: '#a5a5a5' }, nls.localize('treeIndentGuidesStroke', "Tree stroke color for the indentation guides.")); +export const tableColumnsBorder = registerColor('tree.tableColumnsBorder', { dark: '#CCCCCC20', light: '#61616120', hcDark: null, hcLight: null }, nls.localize('tableColumnsBorder', "Table border color between columns.")); +export const tableOddRowsBackgroundColor = registerColor('tree.tableOddRowsBackground', { dark: transparent(foreground, 0.04), light: transparent(foreground, 0.04), hcDark: null, hcLight: null }, nls.localize('tableOddRowsBackgroundColor', "Background color for odd table rows.")); +export const listDeemphasizedForeground = registerColor('list.deemphasizedForeground', { dark: '#8C8C8C', light: '#8E8E90', hcDark: '#A7A8A9', hcLight: '#666666' }, nls.localize('listDeemphasizedForeground', "List/Tree foreground color for items that are deemphasized. ")); /** * Quick pick widget (dependent on List and tree colors) */ -export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: null, light: null, hc: null }, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); -export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hc: listActiveSelectionForeground }, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); -export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hc: listActiveSelectionIconForeground }, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); -export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), hc: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); +export const _deprecatedQuickInputListFocusBackground = registerColor('quickInput.list.focusBackground', { dark: null, light: null, hcDark: null, hcLight: null }, '', undefined, nls.localize('quickInput.list.focusBackground deprecation', "Please use quickInputList.focusBackground instead")); +export const quickInputListFocusForeground = registerColor('quickInputList.focusForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, nls.localize('quickInput.listFocusForeground', "Quick picker foreground color for the focused item.")); +export const quickInputListFocusIconForeground = registerColor('quickInputList.focusIconForeground', { dark: listActiveSelectionIconForeground, light: listActiveSelectionIconForeground, hcDark: listActiveSelectionIconForeground, hcLight: listActiveSelectionIconForeground }, nls.localize('quickInput.listFocusIconForeground', "Quick picker icon foreground color for the focused item.")); +export const quickInputListFocusBackground = registerColor('quickInputList.focusBackground', { dark: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), light: oneOf(_deprecatedQuickInputListFocusBackground, listActiveSelectionBackground), hcDark: null, hcLight: null }, nls.localize('quickInput.listFocusBackground', "Quick picker background color for the focused item.")); /** * Menu colors */ -export const menuBorder = registerColor('menu.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('menuBorder', "Border color of menus.")); -export const menuForeground = registerColor('menu.foreground', { dark: selectForeground, light: foreground, hc: selectForeground }, nls.localize('menuForeground', "Foreground color of menu items.")); -export const menuBackground = registerColor('menu.background', { dark: selectBackground, light: selectBackground, hc: selectBackground }, nls.localize('menuBackground', "Background color of menu items.")); -export const menuSelectionForeground = registerColor('menu.selectionForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hc: listActiveSelectionForeground }, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); -export const menuSelectionBackground = registerColor('menu.selectionBackground', { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hc: listActiveSelectionBackground }, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); -export const menuSelectionBorder = registerColor('menu.selectionBorder', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); -export const menuSeparatorBackground = registerColor('menu.separatorBackground', { dark: '#BBBBBB', light: '#888888', hc: contrastBorder }, nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); +export const menuBorder = registerColor('menu.border', { dark: null, light: null, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuBorder', "Border color of menus.")); +export const menuForeground = registerColor('menu.foreground', { dark: selectForeground, light: foreground, hcDark: selectForeground, hcLight: selectForeground }, nls.localize('menuForeground', "Foreground color of menu items.")); +export const menuBackground = registerColor('menu.background', { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, nls.localize('menuBackground', "Background color of menu items.")); +export const menuSelectionForeground = registerColor('menu.selectionForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); +export const menuSelectionBackground = registerColor('menu.selectionBackground', { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); +export const menuSelectionBorder = registerColor('menu.selectionBorder', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); +export const menuSeparatorBackground = registerColor('menu.separatorBackground', { dark: '#BBBBBB', light: '#888888', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); /** * Toolbar colors */ -export const toolbarHoverBackground = registerColor('toolbar.hoverBackground', { dark: '#5a5d5e50', light: '#b8b8b850', hc: null }, nls.localize('toolbarHoverBackground', "Toolbar background when hovering over actions using the mouse")); -export const toolbarHoverOutline = registerColor('toolbar.hoverOutline', { dark: null, light: null, hc: activeContrastBorder }, nls.localize('toolbarHoverOutline', "Toolbar outline when hovering over actions using the mouse")); -export const toolbarActiveBackground = registerColor('toolbar.activeBackground', { dark: lighten(toolbarHoverBackground, 0.1), light: darken(toolbarHoverBackground, 0.1), hc: null }, nls.localize('toolbarActiveBackground', "Toolbar background when holding the mouse over actions")); +export const toolbarHoverBackground = registerColor('toolbar.hoverBackground', { dark: '#5a5d5e50', light: '#b8b8b850', hcDark: null, hcLight: null }, nls.localize('toolbarHoverBackground', "Toolbar background when hovering over actions using the mouse")); +export const toolbarHoverOutline = registerColor('toolbar.hoverOutline', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('toolbarHoverOutline', "Toolbar outline when hovering over actions using the mouse")); +export const toolbarActiveBackground = registerColor('toolbar.activeBackground', { dark: lighten(toolbarHoverBackground, 0.1), light: darken(toolbarHoverBackground, 0.1), hcDark: null, hcLight: null }, nls.localize('toolbarActiveBackground', "Toolbar background when holding the mouse over actions")); /** * Snippet placeholder colors */ -export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hc: new Color(new RGBA(124, 124, 124, 0.3)) }, nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); -export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', { dark: null, light: null, hc: null }, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); -export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', { dark: null, light: null, hc: null }, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); -export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hc: '#525252' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); +export const snippetTabstopHighlightBackground = registerColor('editor.snippetTabstopHighlightBackground', { dark: new Color(new RGBA(124, 124, 124, 0.3)), light: new Color(new RGBA(10, 50, 100, 0.2)), hcDark: new Color(new RGBA(124, 124, 124, 0.3)), hcLight: new Color(new RGBA(10, 50, 100, 0.2)) }, nls.localize('snippetTabstopHighlightBackground', "Highlight background color of a snippet tabstop.")); +export const snippetTabstopHighlightBorder = registerColor('editor.snippetTabstopHighlightBorder', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('snippetTabstopHighlightBorder', "Highlight border color of a snippet tabstop.")); +export const snippetFinalTabstopHighlightBackground = registerColor('editor.snippetFinalTabstopHighlightBackground', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('snippetFinalTabstopHighlightBackground', "Highlight background color of the final tabstop of a snippet.")); +export const snippetFinalTabstopHighlightBorder = registerColor('editor.snippetFinalTabstopHighlightBorder', { dark: '#525252', light: new Color(new RGBA(10, 50, 100, 0.5)), hcDark: '#525252', hcLight: '#292929' }, nls.localize('snippetFinalTabstopHighlightBorder', "Highlight border color of the final tabstop of a snippet.")); /** * Breadcrumb colors */ -export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hc: transparent(foreground, 0.8) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); -export const breadcrumbsBackground = registerColor('breadcrumb.background', { light: editorBackground, dark: editorBackground, hc: editorBackground }, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); -export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hc: lighten(foreground, 0.1) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); -export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hc: lighten(foreground, 0.1) }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); -export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hc: editorWidgetBackground }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); +export const breadcrumbsForeground = registerColor('breadcrumb.foreground', { light: transparent(foreground, 0.8), dark: transparent(foreground, 0.8), hcDark: transparent(foreground, 0.8), hcLight: transparent(foreground, 0.8) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsBackground = registerColor('breadcrumb.background', { light: editorBackground, dark: editorBackground, hcDark: editorBackground, hcLight: editorBackground }, nls.localize('breadcrumbsBackground', "Background color of breadcrumb items.")); +export const breadcrumbsFocusForeground = registerColor('breadcrumb.focusForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsFocusForeground', "Color of focused breadcrumb items.")); +export const breadcrumbsActiveSelectionForeground = registerColor('breadcrumb.activeSelectionForeground', { light: darken(foreground, 0.2), dark: lighten(foreground, 0.1), hcDark: lighten(foreground, 0.1), hcLight: lighten(foreground, 0.1) }, nls.localize('breadcrumbsSelectedForegound', "Color of selected breadcrumb items.")); +export const breadcrumbsPickerBackground = registerColor('breadcrumbPicker.background', { light: editorWidgetBackground, dark: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, nls.localize('breadcrumbsSelectedBackground', "Background color of breadcrumb item picker.")); /** * Merge-conflict colors @@ -491,51 +506,51 @@ const commonBaseColor = Color.fromHex('#606060').transparent(0.4); const contentTransparency = 0.4; const rulerTransparency = 1; -export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBackground', { dark: currentBaseColor, light: currentBaseColor, hc: null }, nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hc: transparent(mergeCurrentHeaderBackground, contentTransparency) }, nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', { dark: incomingBaseColor, light: incomingBaseColor, hc: null }, nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hc: transparent(mergeIncomingHeaderBackground, contentTransparency) }, nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', { dark: commonBaseColor, light: commonBaseColor, hc: null }, nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hc: transparent(mergeCommonHeaderBackground, contentTransparency) }, nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeCurrentHeaderBackground = registerColor('merge.currentHeaderBackground', { dark: currentBaseColor, light: currentBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeCurrentHeaderBackground', 'Current header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeCurrentContentBackground = registerColor('merge.currentContentBackground', { dark: transparent(mergeCurrentHeaderBackground, contentTransparency), light: transparent(mergeCurrentHeaderBackground, contentTransparency), hcDark: transparent(mergeCurrentHeaderBackground, contentTransparency), hcLight: transparent(mergeCurrentHeaderBackground, contentTransparency) }, nls.localize('mergeCurrentContentBackground', 'Current content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeIncomingHeaderBackground = registerColor('merge.incomingHeaderBackground', { dark: incomingBaseColor, light: incomingBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeIncomingHeaderBackground', 'Incoming header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeIncomingContentBackground = registerColor('merge.incomingContentBackground', { dark: transparent(mergeIncomingHeaderBackground, contentTransparency), light: transparent(mergeIncomingHeaderBackground, contentTransparency), hcDark: transparent(mergeIncomingHeaderBackground, contentTransparency), hcLight: transparent(mergeIncomingHeaderBackground, contentTransparency) }, nls.localize('mergeIncomingContentBackground', 'Incoming content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeCommonHeaderBackground = registerColor('merge.commonHeaderBackground', { dark: commonBaseColor, light: commonBaseColor, hcDark: null, hcLight: null }, nls.localize('mergeCommonHeaderBackground', 'Common ancestor header background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); +export const mergeCommonContentBackground = registerColor('merge.commonContentBackground', { dark: transparent(mergeCommonHeaderBackground, contentTransparency), light: transparent(mergeCommonHeaderBackground, contentTransparency), hcDark: transparent(mergeCommonHeaderBackground, contentTransparency), hcLight: transparent(mergeCommonHeaderBackground, contentTransparency) }, nls.localize('mergeCommonContentBackground', 'Common ancestor content background in inline merge-conflicts. The color must not be opaque so as not to hide underlying decorations.'), true); -export const mergeBorder = registerColor('merge.border', { dark: null, light: null, hc: '#C3DF6F' }, nls.localize('mergeBorder', 'Border color on headers and the splitter in inline merge-conflicts.')); +export const mergeBorder = registerColor('merge.border', { dark: null, light: null, hcDark: '#C3DF6F', hcLight: '#007ACC' }, nls.localize('mergeBorder', 'Border color on headers and the splitter in inline merge-conflicts.')); -export const overviewRulerCurrentContentForeground = registerColor('editorOverviewRuler.currentContentForeground', { dark: transparent(mergeCurrentHeaderBackground, rulerTransparency), light: transparent(mergeCurrentHeaderBackground, rulerTransparency), hc: mergeBorder }, nls.localize('overviewRulerCurrentContentForeground', 'Current overview ruler foreground for inline merge-conflicts.')); -export const overviewRulerIncomingContentForeground = registerColor('editorOverviewRuler.incomingContentForeground', { dark: transparent(mergeIncomingHeaderBackground, rulerTransparency), light: transparent(mergeIncomingHeaderBackground, rulerTransparency), hc: mergeBorder }, nls.localize('overviewRulerIncomingContentForeground', 'Incoming overview ruler foreground for inline merge-conflicts.')); -export const overviewRulerCommonContentForeground = registerColor('editorOverviewRuler.commonContentForeground', { dark: transparent(mergeCommonHeaderBackground, rulerTransparency), light: transparent(mergeCommonHeaderBackground, rulerTransparency), hc: mergeBorder }, nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); +export const overviewRulerCurrentContentForeground = registerColor('editorOverviewRuler.currentContentForeground', { dark: transparent(mergeCurrentHeaderBackground, rulerTransparency), light: transparent(mergeCurrentHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerCurrentContentForeground', 'Current overview ruler foreground for inline merge-conflicts.')); +export const overviewRulerIncomingContentForeground = registerColor('editorOverviewRuler.incomingContentForeground', { dark: transparent(mergeIncomingHeaderBackground, rulerTransparency), light: transparent(mergeIncomingHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerIncomingContentForeground', 'Incoming overview ruler foreground for inline merge-conflicts.')); +export const overviewRulerCommonContentForeground = registerColor('editorOverviewRuler.commonContentForeground', { dark: transparent(mergeCommonHeaderBackground, rulerTransparency), light: transparent(mergeCommonHeaderBackground, rulerTransparency), hcDark: mergeBorder, hcLight: mergeBorder }, nls.localize('overviewRulerCommonContentForeground', 'Common ancestor overview ruler foreground for inline merge-conflicts.')); -export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', { dark: '#d186167e', light: '#d186167e', hc: '#AB5A00' }, nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); +export const overviewRulerFindMatchForeground = registerColor('editorOverviewRuler.findMatchForeground', { dark: '#d186167e', light: '#d186167e', hcDark: '#AB5A00', hcLight: '' }, nls.localize('overviewRulerFindMatchForeground', 'Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.'), true); -export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hc: '#A0A0A0CC' }, nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); +export const overviewRulerSelectionHighlightForeground = registerColor('editorOverviewRuler.selectionHighlightForeground', { dark: '#A0A0A0CC', light: '#A0A0A0CC', hcDark: '#A0A0A0CC', hcLight: '#A0A0A0CC' }, nls.localize('overviewRulerSelectionHighlightForeground', 'Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.'), true); -export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hc: '#AB5A00' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); -export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hc: '#ffffff' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); -export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hc: '#ffffff' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); -export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hc: new Color(new RGBA(255, 50, 50, 1)) }, nls.localize('minimapError', 'Minimap marker color for errors.')); -export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); -export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hc: null }, nls.localize('minimapBackground', "Minimap background color.")); -export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hc: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); +export const minimapFindMatch = registerColor('minimap.findMatchHighlight', { light: '#d18616', dark: '#d18616', hcDark: '#AB5A00', hcLight: '#0F4A85' }, nls.localize('minimapFindMatchHighlight', 'Minimap marker color for find matches.'), true); +export const minimapSelectionOccurrenceHighlight = registerColor('minimap.selectionOccurrenceHighlight', { light: '#c9c9c9', dark: '#676767', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionOccurrenceHighlight', 'Minimap marker color for repeating editor selections.'), true); +export const minimapSelection = registerColor('minimap.selectionHighlight', { light: '#ADD6FF', dark: '#264F78', hcDark: '#ffffff', hcLight: '#0F4A85' }, nls.localize('minimapSelectionHighlight', 'Minimap marker color for the editor selection.'), true); +export const minimapError = registerColor('minimap.errorHighlight', { dark: new Color(new RGBA(255, 18, 18, 0.7)), light: new Color(new RGBA(255, 18, 18, 0.7)), hcDark: new Color(new RGBA(255, 50, 50, 1)), hcLight: '#B5200D' }, nls.localize('minimapError', 'Minimap marker color for errors.')); +export const minimapWarning = registerColor('minimap.warningHighlight', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningBorder, hcLight: editorWarningBorder }, nls.localize('overviewRuleWarning', 'Minimap marker color for warnings.')); +export const minimapBackground = registerColor('minimap.background', { dark: null, light: null, hcDark: null, hcLight: null }, nls.localize('minimapBackground', "Minimap background color.")); +export const minimapForegroundOpacity = registerColor('minimap.foregroundOpacity', { dark: Color.fromHex('#000f'), light: Color.fromHex('#000f'), hcDark: Color.fromHex('#000f'), hcLight: Color.fromHex('#000f') }, nls.localize('minimapForegroundOpacity', 'Opacity of foreground elements rendered in the minimap. For example, "#000000c0" will render the elements with 75% opacity.')); -export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hc: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); -export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hc: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); -export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hc: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); +export const minimapSliderBackground = registerColor('minimapSlider.background', { light: transparent(scrollbarSliderBackground, 0.5), dark: transparent(scrollbarSliderBackground, 0.5), hcDark: transparent(scrollbarSliderBackground, 0.5), hcLight: transparent(scrollbarSliderBackground, 0.5) }, nls.localize('minimapSliderBackground', "Minimap slider background color.")); +export const minimapSliderHoverBackground = registerColor('minimapSlider.hoverBackground', { light: transparent(scrollbarSliderHoverBackground, 0.5), dark: transparent(scrollbarSliderHoverBackground, 0.5), hcDark: transparent(scrollbarSliderHoverBackground, 0.5), hcLight: transparent(scrollbarSliderHoverBackground, 0.5) }, nls.localize('minimapSliderHoverBackground', "Minimap slider background color when hovering.")); +export const minimapSliderActiveBackground = registerColor('minimapSlider.activeBackground', { light: transparent(scrollbarSliderActiveBackground, 0.5), dark: transparent(scrollbarSliderActiveBackground, 0.5), hcDark: transparent(scrollbarSliderActiveBackground, 0.5), hcLight: transparent(scrollbarSliderActiveBackground, 0.5) }, nls.localize('minimapSliderActiveBackground', "Minimap slider background color when clicked on.")); -export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); -export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); -export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); +export const problemsErrorIconForeground = registerColor('problemsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, nls.localize('problemsErrorIconForeground', "The color used for the problems error icon.")); +export const problemsWarningIconForeground = registerColor('problemsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, nls.localize('problemsWarningIconForeground', "The color used for the problems warning icon.")); +export const problemsInfoIconForeground = registerColor('problemsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, nls.localize('problemsInfoIconForeground', "The color used for the problems info icon.")); /** * Chart colors */ -export const chartsForeground = registerColor('charts.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('chartsForeground', "The foreground color used in charts.")); -export const chartsLines = registerColor('charts.lines', { dark: transparent(foreground, .5), light: transparent(foreground, .5), hc: transparent(foreground, .5) }, nls.localize('chartsLines', "The color used for horizontal lines in charts.")); -export const chartsRed = registerColor('charts.red', { dark: editorErrorForeground, light: editorErrorForeground, hc: editorErrorForeground }, nls.localize('chartsRed', "The red color used in chart visualizations.")); -export const chartsBlue = registerColor('charts.blue', { dark: editorInfoForeground, light: editorInfoForeground, hc: editorInfoForeground }, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); -export const chartsYellow = registerColor('charts.yellow', { dark: editorWarningForeground, light: editorWarningForeground, hc: editorWarningForeground }, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); -export const chartsOrange = registerColor('charts.orange', { dark: minimapFindMatch, light: minimapFindMatch, hc: minimapFindMatch }, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); -export const chartsGreen = registerColor('charts.green', { dark: '#89D185', light: '#388A34', hc: '#89D185' }, nls.localize('chartsGreen', "The green color used in chart visualizations.")); -export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', light: '#652D90', hc: '#B180D7' }, nls.localize('chartsPurple', "The purple color used in chart visualizations.")); +export const chartsForeground = registerColor('charts.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, nls.localize('chartsForeground', "The foreground color used in charts.")); +export const chartsLines = registerColor('charts.lines', { dark: transparent(foreground, .5), light: transparent(foreground, .5), hcDark: transparent(foreground, .5), hcLight: transparent(foreground, .5) }, nls.localize('chartsLines', "The color used for horizontal lines in charts.")); +export const chartsRed = registerColor('charts.red', { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorErrorForeground, hcLight: editorErrorForeground }, nls.localize('chartsRed', "The red color used in chart visualizations.")); +export const chartsBlue = registerColor('charts.blue', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: editorInfoForeground, hcLight: editorInfoForeground }, nls.localize('chartsBlue', "The blue color used in chart visualizations.")); +export const chartsYellow = registerColor('charts.yellow', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: editorWarningForeground, hcLight: editorWarningForeground }, nls.localize('chartsYellow', "The yellow color used in chart visualizations.")); +export const chartsOrange = registerColor('charts.orange', { dark: minimapFindMatch, light: minimapFindMatch, hcDark: minimapFindMatch, hcLight: minimapFindMatch }, nls.localize('chartsOrange', "The orange color used in chart visualizations.")); +export const chartsGreen = registerColor('charts.green', { dark: '#89D185', light: '#388A34', hcDark: '#89D185', hcLight: '#374e06' }, nls.localize('chartsGreen', "The green color used in chart visualizations.")); +export const chartsPurple = registerColor('charts.purple', { dark: '#B180D7', light: '#652D90', hcDark: '#B180D7', hcLight: '#652D90' }, nls.localize('chartsPurple', "The purple color used in chart visualizations.")); // ----- color functions diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index b69d95e8ad9..7d5306c66de 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,8 +6,8 @@ import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemable, styleFn } from 'vs/base/common/styler'; -import { activeContrastBorder, badgeBackground, badgeForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, breadcrumbsFocusForeground, breadcrumbsForeground, buttonBackground, buttonBorder, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, ColorIdentifier, ColorTransform, ColorValue, contrastBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropBackground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, menuBackground, menuBorder, menuForeground, menuSelectionBackground, menuSelectionBorder, menuSelectionForeground, menuSeparatorBackground, pickerGroupForeground, problemsErrorIconForeground, problemsInfoIconForeground, problemsWarningIconForeground, progressBarBackground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, resolveColorValue, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, selectBackground, selectBorder, selectForeground, selectListBackground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, tableColumnsBorder, tableOddRowsBackgroundColor, textLinkForeground, treeIndentGuidesStroke, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { activeContrastBorder, badgeBackground, badgeForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, breadcrumbsFocusForeground, breadcrumbsForeground, buttonBackground, buttonBorder, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, ColorIdentifier, ColorTransform, ColorValue, contrastBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropBackground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, menuBackground, menuBorder, menuForeground, menuSelectionBackground, menuSelectionBorder, menuSelectionForeground, menuSeparatorBackground, pickerGroupForeground, problemsErrorIconForeground, problemsInfoIconForeground, problemsWarningIconForeground, progressBarBackground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, resolveColorValue, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, selectBackground, selectBorder, selectForeground, selectListBackground, checkboxBackground, checkboxBorder, checkboxForeground, tableColumnsBorder, tableOddRowsBackgroundColor, textLinkForeground, treeIndentGuidesStroke, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; export interface IStyleOverrides { @@ -50,18 +50,18 @@ export function attachStyler(themeService: IThemeServic return themeService.onDidColorThemeChange(applyStyles); } -export interface ICheckboxStyleOverrides extends IStyleOverrides { +export interface IToggleStyleOverrides extends IStyleOverrides { inputActiveOptionBorderColor?: ColorIdentifier; inputActiveOptionForegroundColor?: ColorIdentifier; inputActiveOptionBackgroundColor?: ColorIdentifier; } -export function attachCheckboxStyler(widget: IThemable, themeService: IThemeService, style?: ICheckboxStyleOverrides): IDisposable { +export function attachToggleStyler(widget: IThemable, themeService: IThemeService, style?: IToggleStyleOverrides): IDisposable { return attachStyler(themeService, { inputActiveOptionBorder: style?.inputActiveOptionBorderColor || inputActiveOptionBorder, inputActiveOptionForeground: style?.inputActiveOptionForegroundColor || inputActiveOptionForeground, inputActiveOptionBackground: style?.inputActiveOptionBackgroundColor || inputActiveOptionBackground - } as ICheckboxStyleOverrides, widget); + } as IToggleStyleOverrides, widget); } export interface IBadgeStyleOverrides extends IStyleOverrides { @@ -132,7 +132,7 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer listFocusBackground: style?.listFocusBackground || quickInputListFocusBackground, listInactiveSelectionIconForeground: style?.listInactiveSelectionIconForeground || quickInputListFocusIconForeground, listFocusForeground: style?.listFocusForeground || quickInputListFocusForeground, - listFocusOutline: style?.listFocusOutline || ((theme: IColorTheme) => theme.type === ColorScheme.HIGH_CONTRAST ? activeContrastBorder : Color.transparent), + listFocusOutline: style?.listFocusOutline || ((theme: IColorTheme) => isHighContrast(theme.type) ? activeContrastBorder : Color.transparent), listHoverBackground: style?.listHoverBackground || listHoverBackground, listHoverForeground: style?.listHoverForeground || listHoverForeground, listHoverOutline: style?.listFocusOutline || activeContrastBorder, @@ -353,9 +353,9 @@ export const defaultDialogStyles = { buttonSecondaryHoverBackground: buttonSecondaryHoverBackground, buttonHoverBackground: buttonHoverBackground, buttonBorder: buttonBorder, - checkboxBorder: simpleCheckboxBorder, - checkboxBackground: simpleCheckboxBackground, - checkboxForeground: simpleCheckboxForeground, + checkboxBorder: checkboxBorder, + checkboxBackground: checkboxBackground, + checkboxForeground: checkboxForeground, errorIconForeground: problemsErrorIconForeground, warningIconForeground: problemsWarningIconForeground, infoIconForeground: problemsInfoIconForeground, diff --git a/src/vs/platform/theme/common/theme.ts b/src/vs/platform/theme/common/theme.ts index 90cef457b57..eabe6ecf497 100644 --- a/src/vs/platform/theme/common/theme.ts +++ b/src/vs/platform/theme/common/theme.ts @@ -9,5 +9,10 @@ export enum ColorScheme { DARK = 'dark', LIGHT = 'light', - HIGH_CONTRAST = 'hc' + HIGH_CONTRAST_DARK = 'hcDark', + HIGH_CONTRAST_LIGHT = 'hcLight' +} + +export function isHighContrast(scheme: ColorScheme): boolean { + return scheme === ColorScheme.HIGH_CONTRAST_DARK || scheme === ColorScheme.HIGH_CONTRAST_LIGHT; } diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index e606f3dac6c..83728f26533 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -95,7 +95,8 @@ export const FolderThemeIcon = Codicon.folder; export function getThemeTypeSelector(type: ColorScheme): string { switch (type) { case ColorScheme.DARK: return 'vs-dark'; - case ColorScheme.HIGH_CONTRAST: return 'hc-black'; + case ColorScheme.HIGH_CONTRAST_DARK: return 'hc-black'; + case ColorScheme.HIGH_CONTRAST_LIGHT: return 'hc-light'; default: return 'vs'; } } diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index dad1375e27b..b8f3d32408d 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -134,7 +134,8 @@ export interface TokenStyleDefaults { scopesToProbe?: ProbeScope[]; light?: TokenStyleValue; dark?: TokenStyleValue; - hc?: TokenStyleValue; + hcDark?: TokenStyleValue; + hcLight?: TokenStyleValue; } export interface SemanticTokenDefaultRule { diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index b9dfdff4761..1250096737e 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -12,6 +12,7 @@ import { IPartsSplash } from 'vs/platform/theme/common/themeService'; const DEFAULT_BG_LIGHT = '#FFFFFF'; const DEFAULT_BG_DARK = '#1E1E1E'; const DEFAULT_BG_HC_BLACK = '#000000'; +const DEFAULT_BG_HC_LIGHT = '#FFFFFF'; const THEME_STORAGE_KEY = 'theme'; const THEME_BG_STORAGE_KEY = 'themeBackground'; @@ -48,8 +49,12 @@ export class ThemeMainService implements IThemeMainService { } else { baseTheme = this.stateMainService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; } - - background = (baseTheme === 'hc-black') ? DEFAULT_BG_HC_BLACK : (baseTheme === 'vs' ? DEFAULT_BG_LIGHT : DEFAULT_BG_DARK); + switch (baseTheme) { + case 'vs': background = DEFAULT_BG_LIGHT; break; + case 'hc-black': background = DEFAULT_BG_HC_BLACK; break; + case 'hc-light': background = DEFAULT_BG_HC_LIGHT; break; + default: background = DEFAULT_BG_DARK; + } } if (isMacintosh && background.toUpperCase() === DEFAULT_BG_DARK) { diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 973deb8f383..a667ae9f8f9 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -16,8 +16,18 @@ export const enum UndoRedoElementType { export interface IResourceUndoRedoElement { readonly type: UndoRedoElementType.Resource; + /** + * The resource impacted by this element. + */ readonly resource: URI; + /** + * A user presentable label. May be localized. + */ readonly label: string; + /** + * A code describing the operation. Will not be localized. + */ + readonly code: string; /** * Show a message to the user confirming when trying to undo this element */ @@ -28,8 +38,18 @@ export interface IResourceUndoRedoElement { export interface IWorkspaceUndoRedoElement { readonly type: UndoRedoElementType.Workspace; + /** + * The resources impacted by this element. + */ readonly resources: readonly URI[]; + /** + * A user presentable label. May be localized. + */ readonly label: string; + /** + * A code describing the operation. Will not be localized. + */ + readonly code: string; /** * Show a message to the user confirming when trying to undo this element */ diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts index 1c5bc997eec..df18b1e9958 100644 --- a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -34,6 +34,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Resource, resource: resource, label: 'typing 1', + code: 'typing', undo: () => { undoCall1++; }, redo: () => { redoCall1++; } }; @@ -68,6 +69,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Resource, resource: resource, label: 'typing 2', + code: 'typing', undo: () => { undoCall2++; }, redo: () => { redoCall2++; } }; @@ -99,6 +101,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Resource, resource: resource, label: 'typing 2', + code: 'typing', undo: () => { undoCall3++; }, redo: () => { redoCall3++; } }; @@ -146,6 +149,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Workspace, resources: [resource1, resource2], label: 'typing 1', + code: 'typing', undo: () => { undoCall1++; }, redo: () => { redoCall1++; }, split: () => { @@ -154,6 +158,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Resource, resource: resource1, label: 'typing 1.1', + code: 'typing', undo: () => { undoCall11++; }, redo: () => { redoCall11++; } }, @@ -161,6 +166,7 @@ suite('UndoRedoService', () => { type: UndoRedoElementType.Resource, resource: resource2, label: 'typing 1.2', + code: 'typing', undo: () => { undoCall12++; }, redo: () => { redoCall12++; } } diff --git a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts index 775fd04c1d3..8e2852d182d 100644 --- a/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts +++ b/src/vs/platform/userData/test/browser/fileUserDataProvider.test.ts @@ -25,7 +25,7 @@ class TestEnvironmentService extends AbstractNativeEnvironmentService { constructor(private readonly _appSettingsHome: URI) { super(Object.create(null), Object.create(null), { _serviceBrand: undefined, ...product }); } - override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.userData }); } + override get userRoamingDataHome() { return this._appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } } suite('FileUserDataProvider', () => { @@ -51,9 +51,9 @@ suite('FileUserDataProvider', () => { environmentService = new TestEnvironmentService(userDataHomeOnDisk); - fileUserDataProvider = new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, logService); + fileUserDataProvider = new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService); disposables.add(fileUserDataProvider); - disposables.add(testObject.registerProvider(Schemas.userData, fileUserDataProvider)); + disposables.add(testObject.registerProvider(Schemas.vscodeUserData, fileUserDataProvider)); }); teardown(() => disposables.clear()); @@ -304,13 +304,13 @@ suite('FileUserDataProvider - Watching', () => { let testObject: FileUserDataProvider; const disposables = new DisposableStore(); const rootFileResource = joinPath(ROOT, 'User'); - const rootUserDataResource = rootFileResource.with({ scheme: Schemas.userData }); + const rootUserDataResource = rootFileResource.with({ scheme: Schemas.vscodeUserData }); const fileEventEmitter: Emitter = new Emitter(); disposables.add(fileEventEmitter); setup(() => { - testObject = disposables.add(new FileUserDataProvider(rootFileResource.scheme, new TestFileSystemProvider(fileEventEmitter.event), Schemas.userData, new NullLogService())); + testObject = disposables.add(new FileUserDataProvider(rootFileResource.scheme, new TestFileSystemProvider(fileEventEmitter.event), Schemas.vscodeUserData, new NullLogService())); }); teardown(() => disposables.clear()); diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index 350768d61a4..9b51a3a8b37 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -50,8 +50,10 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer public async findInFrame(windowId: WebviewWindowId, frameName: string, text: string, options: { findNext?: boolean; forward?: boolean }): Promise { const initialFrame = this.getFrameByName(windowId, frameName); - type WebFrameMainWithFindSupport = typeof WebFrameMain & { + type WebFrameMainWithFindSupport = WebFrameMain & { findInFrame?(text: string, findOptions: FindInFrameOptions): void; + on(event: 'found-in-frame', listener: Function): WebFrameMain; + removeListener(event: 'found-in-frame', listener: Function): WebFrameMain; }; const frame = initialFrame as unknown as WebFrameMainWithFindSupport; if (typeof frame.findInFrame === 'function') { @@ -62,17 +64,17 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer const foundInFrameHandler = (_: unknown, result: FoundInFrameResult) => { if (result.finalUpdate) { this._onFoundInFrame.fire(result); - initialFrame.removeListener('found-in-frame', foundInFrameHandler); + frame.removeListener('found-in-frame', foundInFrameHandler); } }; - initialFrame.on('found-in-frame', foundInFrameHandler); + frame.on('found-in-frame', foundInFrameHandler); } } public async stopFindInFrame(windowId: WebviewWindowId, frameName: string, options: { keepSelection?: boolean }): Promise { const initialFrame = this.getFrameByName(windowId, frameName); - type WebFrameMainWithFindSupport = typeof WebFrameMain & { + type WebFrameMainWithFindSupport = WebFrameMain & { stopFindInFrame?(stopOption: 'keepSelection' | 'clearSelection'): void; }; diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index fa636241ef3..b6fe6d5f906 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -175,7 +175,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { const windowSettings = this.configurationService.getValue('window'); - const options: BrowserWindowConstructorOptions = { + const options: BrowserWindowConstructorOptions & { experimentalDarkMode: boolean } = { width: this.windowState.width, height: this.windowState.height, x: this.windowState.x, @@ -193,6 +193,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { spellcheck: false, nativeWindowOpen: true, zoomFactor: zoomLevelToZoomFactor(windowSettings?.zoomLevel), + // Enable experimental css highlight api https://chromestatus.com/feature/5436441440026624 + // Refs https://github.com/microsoft/vscode/issues/140098 + enableBlinkFeatures: 'HighlightAPI', ...this.environmentMainService.sandbox ? // Sandbox @@ -205,7 +208,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { nodeIntegration: true, contextIsolation: false } - } + }, + experimentalDarkMode: true }; // Apply icon to window @@ -514,9 +518,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Telemetry type WindowErrorClassification = { - type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; + type: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The type of window crash to understand the nature of the crash better.' }; + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The reason of the window crash to understand the nature of the crash better.' }; + code: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; owner: 'bpasero'; comment: 'The exit code of the window process to understand the nature of the crash better' }; }; type WindowErrorEvent = { type: WindowError; diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 3656526d871..4915eb0c283 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -12,7 +12,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Emitter, Event } from 'vs/base/common/event'; import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; -import { getResolvedShellEnv } from 'vs/platform/terminal/node/shellEnv'; +import { getResolvedShellEnv } from 'vs/platform/shell/node/shellEnv'; import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteExtensionHostStartParams } from 'vs/platform/remote/common/remoteAgentConnection'; import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index cbaa32ca6c2..4b9805a64d1 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -25,7 +25,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics import { basename, isAbsolute, join, normalize } from 'vs/base/common/path'; import { ProcessItem } from 'vs/base/common/processes'; import { IBuiltInExtension } from 'vs/base/common/product'; -import { IExtensionManagementCLIService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementCLIService, IExtensionManagementService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { cwd } from 'vs/base/common/process'; import * as pfs from 'vs/base/node/pfs'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -59,6 +59,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { private readonly _connectionToken: ServerConnectionToken, private readonly environmentService: IServerEnvironmentService, extensionManagementCLIService: IExtensionManagementCLIService, + private readonly _extensionManagementService: IExtensionManagementService, private readonly logService: ILogService, private readonly productService: IProductService, private readonly extensionHostStatusService: IExtensionHostStatusService, @@ -332,6 +333,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { extensionHostLogsPath: URI.file(join(this.environmentService.logsPath, `exthost${RemoteAgentEnvironmentChannel._namePool++}`)), globalStorageHome: this.environmentService.globalStorageHome, workspaceStorageHome: this.environmentService.workspaceStorageHome, + localHistoryHome: this.environmentService.localHistoryHome, userHome: this.environmentService.userHome, os: platform.OS, arch: process.arch, @@ -395,10 +397,9 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return r; } - private _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise { - + private async _scanDevelopedExtensions(language: string, translations: Translations, extensionDevelopmentPaths?: string[]): Promise { if (extensionDevelopmentPaths) { - + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); const extDescsP = extensionDevelopmentPaths.map(extDevPath => { return ExtensionScanner.scanOneOrMultipleExtensions( new ExtensionScannerInput( @@ -410,30 +411,31 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { extDevPath, false, // isBuiltin true, // isUnderDevelopment + targetPlatform, translations // translations ), this._extensionScannerHost ); }); - return Promise.all(extDescsP).then((extDescArrays: IExtensionDescription[][]) => { - let extDesc: IExtensionDescription[] = []; - for (let eds of extDescArrays) { - extDesc = extDesc.concat(eds); - } - return extDesc; - }); + const extDescArrays = await Promise.all(extDescsP); + let extDesc: IExtensionDescription[] = []; + for (let eds of extDescArrays) { + extDesc = extDesc.concat(eds); + } + return extDesc; } - return Promise.resolve([]); + return []; } - private _scanBuiltinExtensions(language: string, translations: Translations): Promise { + private async _scanBuiltinExtensions(language: string, translations: Translations): Promise { const version = this.productService.version; const commit = this.productService.commit; const date = this.productService.date; const devMode = !!process.env['VSCODE_DEV']; + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, translations); + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations); const builtinExtensions = ExtensionScanner.scanExtensions(input, this._extensionScannerHost); let finalBuiltinExtensions: Promise = builtinExtensions; @@ -450,7 +452,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { const builtInExtensions = Promise.resolve(this.productService.builtInExtensions || []); - const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, {}); + const input = new ExtensionScannerInput(version, date, commit, language, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, {}); const extraBuiltinExtensions = builtInExtensions .then((builtInExtensions) => new ExtraBuiltInExtensionResolver(builtInExtensions)) .then(resolver => ExtensionScanner.scanExtensions(input, this._extensionScannerHost, resolver)); @@ -461,7 +463,8 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { return finalBuiltinExtensions; } - private _scanInstalledExtensions(language: string, translations: Translations): Promise { + private async _scanInstalledExtensions(language: string, translations: Translations): Promise { + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); const devMode = !!process.env['VSCODE_DEV']; const input = new ExtensionScannerInput( this.productService.version, @@ -472,13 +475,15 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { this.environmentService.extensionsPath!, false, // isBuiltin false, // isUnderDevelopment + targetPlatform, translations ); return ExtensionScanner.scanExtensions(input, this._extensionScannerHost); } - private _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise { + private async _scanSingleExtension(extensionPath: string, isBuiltin: boolean, language: string, translations: Translations): Promise { + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); const devMode = !!process.env['VSCODE_DEV']; const input = new ExtensionScannerInput( this.productService.version, @@ -489,6 +494,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { extensionPath, isBuiltin, false, // isUnderDevelopment + targetPlatform, translations ); return ExtensionScanner.scanSingleExtension(input, this._extensionScannerHost); diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 11f7e7f7c31..4f1fc930f93 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -51,7 +51,7 @@ declare module vsda { } } -export class RemoteExtensionHostAgentServer extends Disposable { +export class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { private readonly _extHostConnections: { [reconnectionToken: string]: ExtensionHostConnection }; private readonly _managementConnections: { [reconnectionToken: string]: ManagementConnection }; @@ -63,6 +63,7 @@ export class RemoteExtensionHostAgentServer extends Disposable { constructor( private readonly _socketServer: SocketServer, private readonly _connectionToken: ServerConnectionToken, + private readonly _vsdaMod: typeof vsda | null, hasWebClient: boolean, @IServerEnvironmentService private readonly _environmentService: IServerEnvironmentService, @IProductService private readonly _productService: IProductService, @@ -266,14 +267,8 @@ export class RemoteExtensionHostAgentServer extends Disposable { const logPrefix = `[${remoteAddress}][${reconnectionToken.substr(0, 8)}]`; const protocol = new PersistentProtocol(socket); - let validator: vsda.validator; - let signer: vsda.signer; - try { - const vsdaMod = require.__$__nodeRequire('vsda'); - validator = new vsdaMod.validator(); - signer = new vsdaMod.signer(); - } catch (e) { - } + const validator = this._vsdaMod ? new this._vsdaMod.validator() : null; + const signer = this._vsdaMod ? new this._vsdaMod.signer() : null; const enum State { WaitingForAuth, @@ -684,6 +679,19 @@ export async function createServer(address: string | net.AddressInfo | null, arg } }); + const vsdaMod = instantiationService.invokeFunction((accessor) => { + const logService = accessor.get(ILogService); + const hasVSDA = fs.existsSync(join(FileAccess.asFileUri('', require).fsPath, '../node_modules/vsda')); + if (hasVSDA) { + try { + return require.__$__nodeRequire('vsda'); + } catch (err) { + logService.error(err); + } + } + return null; + }); + const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath); if (hasWebClient && address && typeof address !== 'string') { @@ -692,7 +700,7 @@ export async function createServer(address: string | net.AddressInfo | null, arg console.log(`Web UI available at http://localhost${address.port === 80 ? '' : `:${address.port}`}/${queryPart}`); } - const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, hasWebClient); + const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient); perf.mark('code/server/ready'); const currentTime = performance.now(); diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index eb2d2230f48..792b3a0274f 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -236,7 +236,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< }; const cliServer = new CLIServerBase(commandsExecuter, this._logService, ipcHandlePath); - const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, false, args.shouldPersistTerminal, args.workspaceId, args.workspaceName); + const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, args.options, args.shouldPersistTerminal, args.workspaceId, args.workspaceName); this._ptyService.onProcessExit(e => e.id === id && cliServer.dispose()); return { diff --git a/src/vs/server/node/server.main.ts b/src/vs/server/node/server.main.ts index 2555835789e..06d5e36e7e9 100644 --- a/src/vs/server/node/server.main.ts +++ b/src/vs/server/node/server.main.ts @@ -39,6 +39,7 @@ const REMOTE_DATA_FOLDER = args['server-data-dir'] || process.env['VSCODE_AGENT_ const USER_DATA_PATH = join(REMOTE_DATA_FOLDER, 'data'); const APP_SETTINGS_HOME = join(USER_DATA_PATH, 'User'); const GLOBAL_STORAGE_HOME = join(APP_SETTINGS_HOME, 'globalStorage'); +const LOCAL_HISTORY_HOME = join(APP_SETTINGS_HOME, 'History'); const MACHINE_SETTINGS_HOME = join(USER_DATA_PATH, 'Machine'); args['user-data-dir'] = USER_DATA_PATH; const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); @@ -46,7 +47,7 @@ const BUILTIN_EXTENSIONS_FOLDER_PATH = join(APP_ROOT, 'extensions'); args['builtin-extensions-dir'] = BUILTIN_EXTENSIONS_FOLDER_PATH; args['extensions-dir'] = args['extensions-dir'] || join(REMOTE_DATA_FOLDER, 'extensions'); -[REMOTE_DATA_FOLDER, args['extensions-dir'], USER_DATA_PATH, APP_SETTINGS_HOME, MACHINE_SETTINGS_HOME, GLOBAL_STORAGE_HOME].forEach(f => { +[REMOTE_DATA_FOLDER, args['extensions-dir'], USER_DATA_PATH, APP_SETTINGS_HOME, MACHINE_SETTINGS_HOME, GLOBAL_STORAGE_HOME, LOCAL_HISTORY_HOME].forEach(f => { try { if (!fs.existsSync(f)) { fs.mkdirSync(f, { mode: 0o700 }); diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index efcec94e030..d87d1f2ba75 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -33,6 +33,7 @@ export const serverOptions: OptionDescriptions = { 'user-data-dir': OPTIONS['user-data-dir'], 'driver': OPTIONS['driver'], 'disable-telemetry': OPTIONS['disable-telemetry'], + 'disable-workspace-trust': OPTIONS['disable-workspace-trust'], 'file-watcher-polling': { type: 'string', deprecates: ['fileWatcherPolling'] }, 'log': OPTIONS['log'], 'logsPath': OPTIONS['logsPath'], @@ -40,8 +41,11 @@ export const serverOptions: OptionDescriptions = { /* ----- vs code web options ----- */ - 'folder': { type: 'string', deprecationMessage: 'No longer supported. Folder needs to be provided in the browser URL.' }, - 'workspace': { type: 'string', deprecationMessage: 'No longer supported. Workspace needs to be provided in the browser URL.' }, + 'folder': { type: 'string', deprecationMessage: 'No longer supported. Folder needs to be provided in the browser URL or with `default-folder`.' }, + 'workspace': { type: 'string', deprecationMessage: 'No longer supported. Workspace needs to be provided in the browser URL or with `default-workspace`.' }, + + 'default-folder': { type: 'string', description: nls.localize('default-folder', 'The workspace folder to open when no input is specified in the browser URL') }, + 'default-workspace': { type: 'string', description: nls.localize('default-workspace', 'The workspace to open when no input is specified in the browser URL') }, 'enable-sync': { type: 'boolean' }, 'github-auth': { type: 'string' }, @@ -130,6 +134,8 @@ export interface ServerParsedArgs { 'telemetry-level'?: string; + 'disable-workspace-trust'?: boolean; + /* ----- vs code options ----- */ 'user-data-dir'?: string; @@ -145,10 +151,16 @@ export interface ServerParsedArgs { 'force-disable-user-env'?: boolean; /* ----- vs code web options ----- */ - /** @deprecated */ + + 'default-workspace'?: string; + 'default-folder'?: string; + + /** @deprecated, use default-workspace instead */ workspace: string; - /** @deprecated */ + /** @deprecated, use default-folder instead */ folder: string; + + 'enable-sync'?: boolean; 'github-auth'?: string; diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 9d6327cd9e3..32ee86edf9b 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -16,7 +16,7 @@ import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; -import { CredentialsMainService } from 'vs/platform/credentials/node/credentialsMainService'; +import { CredentialsWebMainService } from 'vs/platform/credentials/node/credentialsMainService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadServiceChannelClient } from 'vs/platform/download/common/downloadIpc'; @@ -172,10 +172,11 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IEncryptionMainService, new SyncDescriptor(EncryptionMainService, [machineId])); - services.set(ICredentialsMainService, new SyncDescriptor(CredentialsMainService, [true])); + services.set(ICredentialsMainService, new SyncDescriptor(CredentialsWebMainService)); instantiationService.invokeFunction(accessor => { - const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, logService, productService, extensionHostStatusService); + const extensionManagementService = accessor.get(IExtensionManagementService); + const remoteExtensionEnvironmentChannel = new RemoteAgentEnvironmentChannel(connectionToken, environmentService, extensionManagementCLIService, extensionManagementService, logService, productService, extensionHostStatusService); socketServer.registerChannel('remoteextensionsenvironment', remoteExtensionEnvironmentChannel); const telemetryChannel = new ServerTelemetryChannel(accessor.get(IServerTelemetryService), appInsightsAppender); @@ -188,7 +189,6 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel('request', new RequestChannel(accessor.get(IRequestService))); - const extensionManagementService = accessor.get(IExtensionManagementService); const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)); socketServer.registerChannel('extensions', channel); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 7e6eef8697b..ef78c8e308b 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; +import * as path from 'path'; import * as http from 'http'; import * as url from 'url'; import * as util from 'util'; @@ -15,7 +16,7 @@ import { isLinux } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { extname, dirname, join, normalize } from 'vs/base/common/path'; -import { FileAccess, connectionTokenCookieName, connectionTokenQueryName } from 'vs/base/common/network'; +import { FileAccess, connectionTokenCookieName, connectionTokenQueryName, Schemas } from 'vs/base/common/network'; import { generateUuid } from 'vs/base/common/uuid'; import { IProductService } from 'vs/platform/product/common/productService'; import { ServerConnectionToken, ServerConnectionTokenType } from 'vs/server/node/serverConnectionToken'; @@ -265,6 +266,8 @@ export class WebClientServer { _wrapWebWorkerExtHostInIframe = false; } + const resolveWorkspaceURI = (defaultLocation?: string) => defaultLocation && URI.from({ scheme: Schemas.vscodeRemote, path: path.resolve(defaultLocation), authority: remoteAuthority }); + const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/code/browser/workbench/workbench.html' : 'vs/code/browser/workbench/workbench-dev.html', require).fsPath; const authSessionInfo = !this._environmentService.isBuilt && this._environmentService.args['github-auth'] ? { id: generateUuid(), @@ -278,6 +281,9 @@ export class WebClientServer { _wrapWebWorkerExtHostInIframe, developmentOptions: { enableSmokeTestDriver: this._environmentService.driverHandle === 'web' ? true : undefined }, settingsSyncOptions: !this._environmentService.isBuilt && this._environmentService.args['enable-sync'] ? { enabled: true } : undefined, + enableWorkspaceTrust: !this._environmentService.args['disable-workspace-trust'], + folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), + workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), productConfiguration: >{ embedderIdentifier: 'server-distro', extensionsGallery: this._webExtensionResourceUrlTemplate ? { @@ -296,7 +302,7 @@ export class WebClientServer { 'default-src \'self\';', 'img-src \'self\' https: data: blob:;', 'media-src \'self\';', - `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-Luz5WwVrEgqx3ZT5ekNejY0UMaLynWfImiCqdaT6CeQ=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html + `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html 'child-src \'self\';', `frame-src 'self' https://*.vscode-webview.net data:;`, 'worker-src \'self\' data:;', diff --git a/src/vs/server/test/node/serverConnectionToken.test.ts b/src/vs/server/test/node/serverConnectionToken.test.ts index 03964036141..cc3713ad978 100644 --- a/src/vs/server/test/node/serverConnectionToken.test.ts +++ b/src/vs/server/test/node/serverConnectionToken.test.ts @@ -52,7 +52,8 @@ suite('parseServerConnectionToken', () => { assertIsError(await parseServerConnectionToken({ 'connection-token-file': '0', 'connection-token': '0' } as ServerParsedArgs, async () => 'defaultTokenValue')); }); - test('--connection-token-file', async () => { + test('--connection-token-file', async function () { + this.timeout(10000); const testDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'server-connection-token'); fs.mkdirSync(testDir, { recursive: true }); const filename = path.join(testDir, 'connection-token-file'); diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts index 05aae4df438..5264d5f3f15 100644 --- a/src/vs/workbench/api/browser/mainThreadCLICommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; import { isString } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -111,7 +112,9 @@ class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService } protected override validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean { - if (!this._extensionManifestPropertiesService.canExecuteOnWorkspace(manifest)) { + if (!this._extensionManifestPropertiesService.canExecuteOnWorkspace(manifest) + // Web extensions installed on remote can be run in web worker extension host + && !(isWeb && this._extensionManifestPropertiesService.canExecuteOnWeb(manifest))) { output.log(localize('cannot be installed', "Cannot install the '{0}' extension because it is declared to not run in this setup.", getExtensionId(manifest.publisher, manifest.name))); return false; } diff --git a/src/vs/workbench/api/browser/mainThreadComments.ts b/src/vs/workbench/api/browser/mainThreadComments.ts index a9aa93b820c..76ea96af373 100644 --- a/src/vs/workbench/api/browser/mainThreadComments.ts +++ b/src/vs/workbench/api/browser/mainThreadComments.ts @@ -8,12 +8,12 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import * as languages from 'vs/editor/common/languages'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { ICommentInfo, ICommentService, INotebookCommentInfo } from 'vs/workbench/contrib/comments/browser/commentService'; import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol'; import { COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; @@ -24,9 +24,11 @@ import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { localize } from 'vs/nls'; import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { Schemas } from 'vs/base/common/network'; -export class MainThreadCommentThread implements languages.CommentThread { +export class MainThreadCommentThread implements languages.CommentThread { private _input?: languages.CommentInput; get input(): languages.CommentInput | undefined { return this._input; @@ -78,12 +80,12 @@ export class MainThreadCommentThread implements languages.CommentThread { private readonly _onDidChangeComments = new Emitter(); get onDidChangeComments(): Event { return this._onDidChangeComments.event; } - set range(range: IRange) { + set range(range: T) { this._range = range; this._onDidChangeRange.fire(this._range); } - get range(): IRange { + get range(): T { return this._range; } @@ -98,7 +100,7 @@ export class MainThreadCommentThread implements languages.CommentThread { return this._canReply; } - private readonly _onDidChangeRange = new Emitter(); + private readonly _onDidChangeRange = new Emitter(); public onDidChangeRange = this._onDidChangeRange.event; private _collapsibleState: languages.CommentThreadCollapsibleState | undefined; @@ -120,19 +122,36 @@ export class MainThreadCommentThread implements languages.CommentThread { return this._isDisposed; } + isDocumentCommentThread(): this is languages.CommentThread { + return Range.isIRange(this._range); + } + + private _state: languages.CommentThreadState | undefined; + get state() { + return this._state; + } + + set state(newState: languages.CommentThreadState | undefined) { + this._state = newState; + this._onDidChangeState.fire(this._state); + } + + private readonly _onDidChangeState = new Emitter(); + public onDidChangeState = this._onDidChangeState.event; + constructor( public commentThreadHandle: number, public controllerHandle: number, public extensionId: string, public threadId: string, public resource: string, - private _range: IRange, + private _range: T, private _canReply: boolean ) { this._isDisposed = false; } - batchUpdate(changes: CommentThreadChanges) { + batchUpdate(changes: CommentThreadChanges) { const modified = (value: keyof CommentThreadChanges): boolean => Object.prototype.hasOwnProperty.call(changes, value); @@ -142,6 +161,7 @@ export class MainThreadCommentThread implements languages.CommentThread { if (modified('comments')) { this._comments = changes.comments; } if (modified('collapseState')) { this._collapsibleState = changes.collapseState; } if (modified('canReply')) { this.canReply = changes.canReply!; } + if (modified('state')) { this.state = changes.state!; } } dispose() { @@ -151,6 +171,7 @@ export class MainThreadCommentThread implements languages.CommentThread { this._onDidChangeInput.dispose(); this._onDidChangeLabel.dispose(); this._onDidChangeRange.dispose(); + this._onDidChangeState.dispose(); } toJSON(): any { @@ -197,8 +218,8 @@ export class MainThreadCommentController { return this._features.options; } - private readonly _threads: Map = new Map(); - public activeCommentThread?: MainThreadCommentThread; + private readonly _threads: Map> = new Map>(); + public activeCommentThread?: MainThreadCommentThread; get features(): CommentProviderFeatures { return this._features; @@ -222,8 +243,8 @@ export class MainThreadCommentController { commentThreadHandle: number, threadId: string, resource: UriComponents, - range: IRange, - ): languages.CommentThread { + range: IRange | ICellRange, + ): languages.CommentThread { let thread = new MainThreadCommentThread( commentThreadHandle, this.handle, @@ -294,7 +315,7 @@ export class MainThreadCommentController { this._commentService.updateCommentingRanges(this._uniqueId); } - private getKnownThread(commentThreadHandle: number): MainThreadCommentThread { + private getKnownThread(commentThreadHandle: number): MainThreadCommentThread { const thread = this._threads.get(commentThreadHandle); if (!thread) { throw new Error('unknown thread'); @@ -303,7 +324,19 @@ export class MainThreadCommentController { } async getDocumentComments(resource: URI, token: CancellationToken) { - let ret: languages.CommentThread[] = []; + if (resource.scheme === Schemas.vscodeNotebookCell) { + return { + owner: this._uniqueId, + label: this.label, + threads: [], + commentingRanges: { + resource: resource, + ranges: [] + } + }; + } + + let ret: languages.CommentThread[] = []; for (let thread of [...this._threads.keys()]) { const commentThread = this._threads.get(thread)!; if (commentThread.resource === resource.toString()) { @@ -324,6 +357,30 @@ export class MainThreadCommentController { }; } + async getNotebookComments(resource: URI, token: CancellationToken) { + if (resource.scheme !== Schemas.vscodeNotebookCell) { + return { + owner: this._uniqueId, + label: this.label, + threads: [] + }; + } + + let ret: languages.CommentThread[] = []; + for (let thread of [...this._threads.keys()]) { + const commentThread = this._threads.get(thread)!; + if (commentThread.resource === resource.toString()) { + ret.push(commentThread); + } + } + + return { + owner: this._uniqueId, + label: this.label, + threads: ret + }; + } + async getCommentingRanges(resource: URI, token: CancellationToken): Promise { let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token); return commentingRanges || []; @@ -333,8 +390,8 @@ export class MainThreadCommentController { return this._proxy.$toggleReaction(this._handle, thread.commentThreadHandle, uri, comment, reaction); } - getAllComments(): MainThreadCommentThread[] { - let ret: MainThreadCommentThread[] = []; + getAllComments(): MainThreadCommentThread[] { + let ret: MainThreadCommentThread[] = []; for (let thread of [...this._threads.keys()]) { ret.push(this._threads.get(thread)!); } @@ -369,7 +426,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments private _handlers = new Map(); private _commentControllers = new Map(); - private _activeCommentThread?: MainThreadCommentThread; + private _activeCommentThread?: MainThreadCommentThread; private readonly _activeCommentThreadDisposables = this._register(new DisposableStore()); private _openViewListener: IDisposable | null = null; @@ -385,7 +442,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments); this._register(this._commentService.onDidChangeActiveCommentThread(async thread => { - let handle = (thread as MainThreadCommentThread).controllerHandle; + let handle = (thread as MainThreadCommentThread).controllerHandle; let controller = this._commentControllers.get(handle); if (!controller) { @@ -393,7 +450,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments } this._activeCommentThreadDisposables.clear(); - this._activeCommentThread = thread as MainThreadCommentThread; + this._activeCommentThread = thread as MainThreadCommentThread; controller.activeCommentThread = this._activeCommentThread; })); } @@ -441,9 +498,9 @@ export class MainThreadComments extends Disposable implements MainThreadComments commentThreadHandle: number, threadId: string, resource: UriComponents, - range: IRange, + range: IRange | ICellRange, extensionId: ExtensionIdentifier - ): languages.CommentThread | undefined { + ): languages.CommentThread | undefined { let provider = this._commentControllers.get(handle); if (!provider) { @@ -570,7 +627,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments return this._handlers.get(handle)!; } - $onDidCommentThreadsChange(handle: number, event: languages.CommentThreadChangedEvent) { + $onDidCommentThreadsChange(handle: number, event: languages.CommentThreadChangedEvent) { // notify comment service const providerId = this.getHandler(handle); this._commentService.updateComments(providerId, event); diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 27c48ff72c5..6f247115e34 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -38,7 +38,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopySaveEvent, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -447,6 +447,9 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private readonly _onDidSave: Emitter = this._register(new Emitter()); + readonly onDidSave: Event = this._onDidSave.event; + readonly onDidChangeReadonly = Event.None; //#endregion @@ -477,6 +480,7 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom type: UndoRedoElementType.Resource, resource: this._editorResource, label: label ?? localize('defaultEditLabel', "Edit"), + code: 'undoredo.customEditorEdit', undo: () => this.undo(), redo: () => this.redo(), }); @@ -567,7 +571,14 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom } public async save(options?: ISaveOptions): Promise { - return !!await this.saveCustomEditor(options); + const result = !!await this.saveCustomEditor(options); + + // Emit Save Event + if (result) { + this._onDidSave.fire({ reason: options?.reason, source: options?.source }); + } + + return result; } public async saveCustomEditor(options?: ISaveOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 1fb6d0b37f7..5cefac453b2 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -15,6 +15,7 @@ import severity from 'vs/base/common/severity'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { convertToVSCPaths, convertToDAPaths, isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; @extHostNamedCustomer(MainContext.MainThreadDebugService) export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory { @@ -235,7 +236,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb const saveBeforeStart = typeof options.suppressSaveBeforeStart === 'boolean' ? !options.suppressSaveBeforeStart : undefined; return this.debugService.startDebugging(launch, nameOrConfig, debugOptions, saveBeforeStart); } catch (err) { - throw new Error(err && err.message ? err.message : 'cannot start debugging'); + throw new ErrorNoTelemetry(err && err.message ? err.message : 'cannot start debugging'); } } @@ -253,11 +254,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb if (response && response.success) { return response.body; } else { - return Promise.reject(new Error(response ? response.message : 'custom request failed')); + return Promise.reject(new ErrorNoTelemetry(response ? response.message : 'custom request failed')); } }); } - return Promise.reject(new Error('debug session not found')); + return Promise.reject(new ErrorNoTelemetry('debug session not found')); } public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise { @@ -265,7 +266,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb if (session) { return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId)); } - return Promise.reject(new Error('debug session not found')); + return Promise.reject(new ErrorNoTelemetry('debug session not found')); } public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { @@ -277,7 +278,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb } else { // stop all return this.debugService.stopSession(undefined); } - return Promise.reject(new Error('debug session not found')); + return Promise.reject(new ErrorNoTelemetry('debug session not found')); } public $appendDebugConsole(value: string): void { diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 256b1b08bf9..0e64e46b917 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -214,7 +214,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)) { - new Error(`Invalid uri. Scheme and authority or path must be set.`); + throw new Error(`Invalid uri. Scheme and authority or path must be set.`); } const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri); diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 0b56ed07df4..f24b5ecb04d 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -420,6 +420,15 @@ export class MainThreadDocumentsAndEditors { return undefined; } + getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined { + for (const [id, editor] of this._textEditors) { + if (editor.getCodeEditor() === codeEditor) { + return id; + } + } + return undefined; + } + getEditor(id: string): MainThreadTextEditor | undefined { return this._textEditors.get(id); } diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 360ee15f631..d4736a0196d 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -4,24 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { DisposableStore } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, TabKind } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, IExtHostEditorTabsShape, MainContext, IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, AnyInputDto, TabInputKind } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { EditorResourceAccessor, IUntypedEditorInput, SideBySideEditor, GroupModelChangeKind } from 'vs/workbench/common/editor'; +import { GroupModelChangeKind } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { GroupDirection, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorsChangeEvent, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; + +interface TabInfo { + tab: IEditorTabDto; + group: IEditorGroup; + editorInput: EditorInput; +} @extHostNamedCustomer(MainContext.MainThreadEditorTabs) -export class MainThreadEditorTabs { +export class MainThreadEditorTabs implements MainThreadEditorTabsShape { private readonly _dispoables = new DisposableStore(); private readonly _proxy: IExtHostEditorTabsShape; + // List of all groups and their corresponding tabs, this is **the** model private _tabGroupModel: IEditorTabGroupDto[] = []; - private readonly _groupModel: Map = new Map(); + // Lookup table for finding group by id + private readonly _groupLookup: Map = new Map(); + // Lookup table for finding tab by id + private readonly _tabInfoLookup: Map = new Map(); constructor( extHostContext: IExtHostContext, @@ -31,12 +42,14 @@ export class MainThreadEditorTabs { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs); - // Queue all events that arrive on the same event loop and then send them as a batch + // Main listener which responds to events from the editor service this._dispoables.add(editorService.onDidEditorsChange((event) => this._updateTabsModel(event))); this._editorGroupsService.whenReady.then(() => this._createTabsModel()); } dispose(): void { + this._groupLookup.clear(); + this._tabInfoLookup.clear(); this._dispoables.dispose(); } @@ -47,45 +60,74 @@ export class MainThreadEditorTabs { * @returns A tab object */ private _buildTabObject(group: IEditorGroup, editor: EditorInput, editorIndex: number): IEditorTabDto { - // Even though the id isn't a diff / sideBySide on the main side we need to let the ext host know what type of editor it is const editorId = editor.editorId; - const tabKind = editor instanceof DiffEditorInput ? TabKind.Diff : editor instanceof SideBySideEditorInput ? TabKind.SidebySide : TabKind.Singular; const tab: IEditorTabDto = { - viewColumn: editorGroupToColumn(this._editorGroupsService, group), + id: this._generateTabId(editor, group.id), label: editor.getName(), - resource: editor instanceof SideBySideEditorInput ? EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }) : EditorResourceAccessor.getCanonicalUri(editor), editorId, - kind: tabKind, - additionalResourcesAndViewIds: [], + input: this._editorInputToDto(editor), isPinned: group.isSticky(editorIndex), + isPreview: group.isPinned(editorIndex), isActive: group.isActive(editor), isDirty: editor.isDirty() }; - tab.additionalResourcesAndViewIds.push({ resource: tab.resource, viewId: tab.editorId }); - if (editor instanceof SideBySideEditorInput) { - tab.additionalResourcesAndViewIds.push({ resource: EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.SECONDARY }), viewId: editor.primary.editorId ?? editor.editorId }); - } return tab; } + private _editorInputToDto(editor: EditorInput): AnyInputDto { - private _tabToUntypedEditorInput(tab: IEditorTabDto): IUntypedEditorInput { - if (tab.kind !== TabKind.Diff && tab.kind !== TabKind.SidebySide) { - return { resource: URI.revive(tab.resource), options: { override: tab.editorId } }; - } else if (tab.kind === TabKind.SidebySide) { + if (editor instanceof AbstractTextResourceEditorInput) { return { - options: { override: tab.editorId }, - primary: { resource: URI.revive(tab.resource), options: { override: tab.editorId } }, - secondary: { resource: URI.revive(tab.additionalResourcesAndViewIds[1].resource), options: { override: tab.additionalResourcesAndViewIds[1].viewId } } - }; - } else { - // Diff case - return { - options: { override: tab.editorId }, - modified: { resource: URI.revive(tab.resource), options: { override: tab.editorId } }, - original: { resource: URI.revive(tab.additionalResourcesAndViewIds[1].resource), options: { override: tab.additionalResourcesAndViewIds[1]?.viewId } } + kind: TabInputKind.TextInput, + uri: editor.resource }; } + + if (editor instanceof NotebookEditorInput) { + return { + kind: TabInputKind.NotebookInput, + notebookType: editor.viewType, + uri: editor.resource + }; + } + + if (editor instanceof CustomEditorInput) { + return { + kind: TabInputKind.CustomEditorInput, + viewType: editor.viewType, + uri: editor.resource, + }; + } + + if (editor instanceof DiffEditorInput) { + if (editor.modified instanceof AbstractTextResourceEditorInput && editor.original instanceof AbstractTextResourceEditorInput) { + return { + kind: TabInputKind.TextDiffInput, + modified: editor.modified.resource, + original: editor.original.resource + }; + } + if (editor.modified instanceof NotebookEditorInput && editor.original instanceof NotebookEditorInput) { + return { + kind: TabInputKind.NotebookDiffInput, + notebookType: editor.original.viewType, + modified: editor.modified.resource, + original: editor.original.resource + }; + } + } + + return { kind: TabInputKind.UnknownInput }; + } + + /** + * Generates a unique id for a tab + * @param editor The editor input + * @param groupId The group id + * @returns A unique identifier for a specific tab + */ + private _generateTabId(editor: EditorInput, groupId: number) { + return `${groupId}~${editor.editorId}-${editor.typeId}-${editor.resource?.toString()}`; } /** @@ -93,8 +135,11 @@ export class MainThreadEditorTabs { */ private _onDidGroupActivate() { const activeGroupId = this._editorGroupsService.activeGroup.id; - for (const group of this._tabGroupModel) { - group.isActive = group.groupId === activeGroupId; + const activeGroup = this._groupLookup.get(activeGroupId); + if (activeGroup) { + // Ok not to loop as exthost accepts last active group + activeGroup.isActive = true; + this._proxy.$acceptTabGroupUpdate(activeGroup); } } @@ -102,12 +147,17 @@ export class MainThreadEditorTabs { * Called when the tab label changes * @param groupId The id of the group the tab exists in * @param editorInput The editor input represented by the tab - * @param editorIndex The index of the editor within that group */ - private _onDidTabLabelChange(groupId: number, editorInput: EditorInput, editorIndex: number) { - const tabs = this._groupModel.get(groupId)?.tabs; - if (tabs) { - tabs[editorIndex].label = editorInput.getName(); + private _onDidTabLabelChange(groupId: number, editorInput: EditorInput) { + const tabId = this._generateTabId(editorInput, groupId); + const tabInfo = this._tabInfoLookup.get(tabId); + // If tab is found patch, else rebuild + if (tabInfo) { + tabInfo.tab.label = editorInput.getName(); + this._proxy.$acceptTabUpdate(groupId, tabInfo.tab); + } else { + console.error('Invalid model for label change, rebuilding'); + this._createTabsModel(); } } @@ -120,17 +170,22 @@ export class MainThreadEditorTabs { private _onDidTabOpen(groupId: number, editorInput: EditorInput, editorIndex: number) { const group = this._editorGroupsService.getGroup(groupId); // Even if the editor service knows about the group the group might not exist yet in our model - const groupInModel = this._groupModel.get(groupId) !== undefined; + const groupInModel = this._groupLookup.get(groupId) !== undefined; // Means a new group was likely created so we rebuild the model if (!group || !groupInModel) { this._createTabsModel(); return; } - const tabs = this._groupModel.get(groupId)?.tabs; + const tabs = this._groupLookup.get(groupId)?.tabs; if (tabs) { // Splice tab into group at index editorIndex - tabs.splice(editorIndex, 0, this._buildTabObject(group, editorInput, editorIndex)); + const tabObject = this._buildTabObject(group, editorInput, editorIndex); + tabs.splice(editorIndex, 0, tabObject); + // Update lookup + this._tabInfoLookup.set(this._generateTabId(editorInput, groupId), { group, editorInput, tab: tabObject }); } + // TODO @lramos15 Switch to patching here + this._proxy.$acceptEditorTabModel(this._tabGroupModel); } /** @@ -140,25 +195,29 @@ export class MainThreadEditorTabs { */ private _onDidTabClose(groupId: number, editorIndex: number) { const group = this._editorGroupsService.getGroup(groupId); - const tabs = this._groupModel.get(groupId)?.tabs; + const tabs = this._groupLookup.get(groupId)?.tabs; // Something is wrong with the model state so we rebuild if (!group || !tabs) { this._createTabsModel(); return; } // Splice tab into group at index editorIndex - tabs.splice(editorIndex, 1); + const removedTab = tabs.splice(editorIndex, 1); + // Update lookup + this._tabInfoLookup.delete(removedTab[0]?.id ?? ''); - // If no tabs it's an empty group and gets deleted from the model + // If no tabs left, it's an empty group and the group gets deleted from the model // In the future we may want to support empty groups if (tabs.length === 0) { for (let i = 0; i < this._tabGroupModel.length; i++) { if (this._tabGroupModel[i].groupId === group.id) { this._tabGroupModel.splice(i, 1); - return; + this._groupLookup.delete(group.id); } } } + // TODO @lramos15 Switch to patching here + this._proxy.$acceptEditorTabModel(this._tabGroupModel); } /** @@ -167,22 +226,17 @@ export class MainThreadEditorTabs { * @param editorIndex The index of the tab */ private _onDidTabActiveChange(groupId: number, editorIndex: number) { - const tabs = this._groupModel.get(groupId)?.tabs; + // TODO @lramos15 use the tab lookup here if possible. Do we have an editor input?! + const tabs = this._groupLookup.get(groupId)?.tabs; if (!tabs) { return; } - let activeTab: IEditorTabDto | undefined; - for (let i = 0; i < tabs.length; i++) { - if (i === editorIndex) { - tabs[i].isActive = true; - activeTab = tabs[i]; - } else { - tabs[i].isActive = false; - } - } - // null assertion is ok here because if tabs is undefined then we would've returned above. - // Therefore there must be a group here. - this._groupModel.get(groupId)!.activeTab = activeTab; + const activeTab = tabs[editorIndex]; + // No need to loop over as the exthost uses the most recently marked active tab + activeTab.isActive = true; + // Send DTO update to the exthost + this._proxy.$acceptTabUpdate(groupId, activeTab); + } /** @@ -192,30 +246,59 @@ export class MainThreadEditorTabs { * @param editor The editor input represented by the tab */ private _onDidTabDirty(groupId: number, editorIndex: number, editor: EditorInput) { - const tab = this._groupModel.get(groupId)?.tabs[editorIndex]; + const tab = this._groupLookup.get(groupId)?.tabs[editorIndex]; // Something wrong with the model staate so we rebuild if (!tab) { + console.error('Invalid model for dirty change, rebuilding'); this._createTabsModel(); return; } tab.isDirty = editor.isDirty(); + this._proxy.$acceptTabUpdate(groupId, tab); } /** - * Called when the tab is pinned / unpinned + * Called when the tab is pinned/unpinned * @param groupId The id of the group the tab is in * @param editorIndex The index of the tab * @param editor The editor input represented by the tab */ - private _onDidTabStickyChange(groupId: number, editorIndex: number, editor: EditorInput) { - const group = this._editorGroupsService.getGroup(groupId); - const tab = this._groupModel.get(groupId)?.tabs[editorIndex]; - // Something wrong with the model staate so we rebuild + private _onDidTabPinChange(groupId: number, editorIndex: number, editor: EditorInput) { + const tabId = this._generateTabId(editor, groupId); + const tabInfo = this._tabInfoLookup.get(tabId); + const group = tabInfo?.group; + const tab = tabInfo?.tab; + // Something wrong with the model state so we rebuild if (!group || !tab) { + console.error('Invalid model for sticky change, rebuilding'); this._createTabsModel(); return; } + // Whether or not the tab has the pin icon (internally it's called sticky) tab.isPinned = group.isSticky(editorIndex); + this._proxy.$acceptTabUpdate(groupId, tab); + } + + /** + * Called when the tab is preview / unpreviewed + * @param groupId The id of the group the tab is in + * @param editorIndex The index of the tab + * @param editor The editor input represented by the tab + */ + private _onDidTabPreviewChange(groupId: number, editorIndex: number, editor: EditorInput) { + const tabId = this._generateTabId(editor, groupId); + const tabInfo = this._tabInfoLookup.get(tabId); + const group = tabInfo?.group; + const tab = tabInfo?.tab; + // Something wrong with the model state so we rebuild + if (!group || !tab) { + console.error('Invalid model for sticky change, rebuilding'); + this._createTabsModel(); + return; + } + // Whether or not the tab has the pin icon (internally it's called sticky) + tab.isPreview = group.isPinned(editorIndex); + this._proxy.$acceptTabUpdate(groupId, tab); } /** @@ -223,29 +306,33 @@ export class MainThreadEditorTabs { */ private _createTabsModel(): void { this._tabGroupModel = []; - this._groupModel.clear(); + this._groupLookup.clear(); + this._tabInfoLookup.clear(); let tabs: IEditorTabDto[] = []; for (const group of this._editorGroupsService.groups) { const currentTabGroupModel: IEditorTabGroupDto = { groupId: group.id, isActive: group.id === this._editorGroupsService.activeGroup.id, viewColumn: editorGroupToColumn(this._editorGroupsService, group), - activeTab: undefined, tabs: [] }; group.editors.forEach((editor, editorIndex) => { const tab = this._buildTabObject(group, editor, editorIndex); - // Mark the tab active within the group - if (tab.isActive) { - currentTabGroupModel.activeTab = tab; - } tabs.push(tab); + // Add information about the tab to the lookup + this._tabInfoLookup.set(this._generateTabId(editor, group.id), { + group, + tab, + editorInput: editor + }); }); currentTabGroupModel.tabs = tabs; this._tabGroupModel.push(currentTabGroupModel); - this._groupModel.set(group.id, currentTabGroupModel); + this._groupLookup.set(group.id, currentTabGroupModel); tabs = []; } + // notify the ext host of the new model + this._proxy.$acceptEditorTabModel(this._tabGroupModel); } // TODOD @lramos15 Remove this after done finishing the tab model code @@ -282,8 +369,8 @@ export class MainThreadEditorTabs { return; } case GroupModelChangeKind.EDITOR_LABEL: - if (event.editor !== undefined && event.editorIndex !== undefined) { - this._onDidTabLabelChange(event.groupId, event.editor, event.editorIndex); + if (event.editor !== undefined) { + this._onDidTabLabelChange(event.groupId, event.editor); break; } case GroupModelChangeKind.EDITOR_OPEN: @@ -308,26 +395,34 @@ export class MainThreadEditorTabs { } case GroupModelChangeKind.EDITOR_STICKY: if (event.editorIndex !== undefined && event.editor !== undefined) { - this._onDidTabStickyChange(event.groupId, event.editorIndex, event.editor); + this._onDidTabPinChange(event.groupId, event.editorIndex, event.editor); + break; + } + case GroupModelChangeKind.EDITOR_PIN: + if (event.editorIndex !== undefined && event.editor !== undefined) { + this._onDidTabPreviewChange(event.groupId, event.editorIndex, event.editor); break; } default: // If it's not an optimized case we rebuild the tabs model from scratch this._createTabsModel(); } - // notify the ext host of the new model - this._proxy.$acceptEditorTabModel(this._tabGroupModel); } //#region Messages received from Ext Host - $moveTab(tab: IEditorTabDto, index: number, viewColumn: EditorGroupColumn): void { + $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void { const groupId = columnToEditorGroup(this._editorGroupsService, viewColumn); + const tabInfo = this._tabInfoLookup.get(tabId); + const tab = tabInfo?.tab; + if (!tab) { + throw new Error(`Attempted to close tab with id ${tabId} which does not exist`); + } let targetGroup: IEditorGroup | undefined; - const sourceGroup = this._editorGroupsService.getGroup(columnToEditorGroup(this._editorGroupsService, tab.viewColumn)); + const sourceGroup = this._editorGroupsService.getGroup(tabInfo.group.id); if (!sourceGroup) { return; } // If group index is out of bounds then we make a new one that's to the right of the last group - if (this._groupModel.get(groupId) === undefined) { + if (this._groupLookup.get(groupId) === undefined) { targetGroup = this._editorGroupsService.addGroup(this._editorGroupsService.groups[this._editorGroupsService.groups.length - 1], GroupDirection.RIGHT, undefined); } else { targetGroup = this._editorGroupsService.getGroup(groupId); @@ -341,26 +436,37 @@ export class MainThreadEditorTabs { index = targetGroup.editors.length; } // Find the correct EditorInput using the tab info - const editorInput = sourceGroup.editors.find(editor => editor.matches(this._tabToUntypedEditorInput(tab))); + const editorInput = tabInfo?.editorInput; if (!editorInput) { return; } // Move the editor to the target group - sourceGroup.moveEditor(editorInput, targetGroup, { index, preserveFocus: true }); + sourceGroup.moveEditor(editorInput, targetGroup, { index, preserveFocus }); return; } - async $closeTab(tab: IEditorTabDto): Promise { - const group = this._editorGroupsService.getGroup(columnToEditorGroup(this._editorGroupsService, tab.viewColumn)); - if (!group) { - return; + async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { + const groups: Map = new Map(); + for (const tabId of tabIds) { + const tabInfo = this._tabInfoLookup.get(tabId); + const tab = tabInfo?.tab; + const group = tabInfo?.group; + const editorTab = tabInfo?.editorInput; + // If not found skip + if (!group || !tab || !tabInfo || !editorTab) { + continue; + } + const groupEditors = groups.get(group); + if (!groupEditors) { + groups.set(group, [editorTab]); + } else { + groupEditors.push(editorTab); + } } - const editorTab = this._tabToUntypedEditorInput(tab); - const editor = group.editors.find(editor => editor.matches(editorTab)); - if (!editor) { - return; + // Loop over keys of the groups map and call closeEditors + for (const [group, editors] of groups) { + group.closeEditors(editors, { preserveFocus }); } - await group.closeEditor(editor); } //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 5e10873b485..7606bc7001e 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -29,7 +29,11 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { ILineChange } from 'vs/editor/common/diff/diffComputer'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { IEditorPane } from 'vs/workbench/common/editor'; +import { IEditorControl } from 'vs/workbench/common/editor'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { if (!data?.edits) { @@ -51,7 +55,8 @@ export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): Re export interface IMainThreadEditorLocator { getEditor(id: string): MainThreadTextEditor | undefined; - findTextEditorIdFor(editorPane: IEditorPane): string | undefined; + findTextEditorIdFor(editorControl: IEditorControl): string | undefined; + getIdOfCodeEditor(codeEditor: ICodeEditor): string | undefined; } export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -64,6 +69,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { private _textEditorsListenersMap: { [editorId: string]: IDisposable[] }; private _editorPositionData: ITextEditorPositionData | null; private _registeredDecorationTypes: { [decorationType: string]: boolean }; + private readonly _dropIntoEditorListeners = new Map(); constructor( private readonly _editorLocator: IMainThreadEditorLocator, @@ -71,7 +77,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IEditorService private readonly _editorService: IEditorService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, ) { this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); @@ -83,6 +89,21 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors())); this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors())); + const registerDropListenerOnEditor = (editor: ICodeEditor) => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + this._dropIntoEditorListeners.set(editor, editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.dataTransfer))); + }; + + this._toDispose.add(_codeEditorService.onCodeEditorAdd(registerDropListenerOnEditor)); + + this._toDispose.add(_codeEditorService.onCodeEditorRemove(editor => { + this._dropIntoEditorListeners.get(editor)?.dispose(); + })); + + for (const editor of this._codeEditorService.listCodeEditors()) { + registerDropListenerOnEditor(editor); + } + this._registeredDecorationTypes = Object.create(null); } @@ -95,6 +116,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { for (let decorationType in this._registeredDecorationTypes) { this._codeEditorService.removeDecorationType(decorationType); } + dispose(this._dropIntoEditorListeners.values()); + this._dropIntoEditorListeners.clear(); this._registeredDecorationTypes = Object.create(null); } @@ -134,6 +157,31 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return result; } + private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dataTransfer: DataTransfer) { + const id = this._editorLocator.getIdOfCodeEditor(editor); + if (typeof id !== 'string') { + return; + } + + const textEditorDataTransfer: IDataTransfer = new Map(); + + for (const item of dataTransfer.items) { + if (item.kind === 'string') { + const type = item.type; + const asStringValue = new Promise(resolve => item.getAsString(resolve)); + textEditorDataTransfer.set(type, { + asString: () => asStringValue, + value: undefined + }); + } + } + + if (textEditorDataTransfer.size > 0) { + const dataTransferDto = await DataTransferConverter.toDataTransferDTO(textEditorDataTransfer); + return this._proxy.$textEditorHandleDrop(id, position, dataTransferDto); + } + } + // --- from extension host process async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index a77cf0e46e6..95d3b69e79d 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -197,9 +197,9 @@ class ExtensionHostProxy implements IExtensionHostProxy { resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { return this._actual.$resolveAuthority(remoteAuthority, resolveAttempt); } - async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); - return URI.revive(uriComponents); + return (uriComponents ? URI.revive(uriComponents) : uriComponents); } startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { return this._actual.$startExtensionHost(enabledExtensionIds); diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index dfcb29b1f39..db80199bc2f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -15,6 +15,8 @@ import { INotebookCellStatusBarItemProvider, INotebookContributionData, Notebook import { INotebookContentProvider, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookShape, MainContext, MainThreadNotebookShape } from '../common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; +import { StopWatch } from 'vs/base/common/stopwatch'; @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks implements MainThreadNotebookShape { @@ -30,6 +32,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { extHostContext: IExtHostContext, @INotebookService private readonly _notebookService: INotebookService, @INotebookCellStatusBarService private readonly _cellStatusBarService: INotebookCellStatusBarService, + @ILogService private readonly _logService: ILogService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); } @@ -107,11 +110,17 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { const registration = this._notebookService.registerNotebookSerializer(viewType, extension, { options, dataToNotebook: async (data: VSBuffer): Promise => { + const sw = new StopWatch(true); const dto = await this._proxy.$dataToNotebook(handle, data, CancellationToken.None); - return NotebookDto.fromNotebookDataDto(dto.value); + const result = NotebookDto.fromNotebookDataDto(dto.value); + this._logService.trace('[NotebookSerializer] dataToNotebook DONE', extension.id, sw.elapsed()); + return result; }, notebookToData: (data: NotebookData): Promise => { - return this._proxy.$notebookToData(handle, new SerializableObjectWithBuffers(NotebookDto.toNotebookDataDto(data)), CancellationToken.None); + const sw = new StopWatch(true); + const result = this._proxy.$notebookToData(handle, new SerializableObjectWithBuffers(NotebookDto.toNotebookDataDto(data)), CancellationToken.None); + this._logService.trace('[NotebookSerializer] notebookToData DONE', extension.id, sw.elapsed()); + return result; } }); const disposables = new DisposableStore(); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts index 693ab628076..c586ceacf35 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocuments.ts @@ -97,16 +97,18 @@ export class MainThreadNotebookDocuments implements MainThreadNotebookDocumentsS } } + const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); + // using the model resolver service to know if the model is dirty or not. // assuming this is the first listener it can mean that at first the model // is marked as dirty and that another event is fired this._proxy.$acceptModelChanged( textModel.uri, new SerializableObjectWithBuffers(eventDto), - this._notebookEditorModelResolverService.isDirty(textModel.uri) + this._notebookEditorModelResolverService.isDirty(textModel.uri), + hasDocumentMetadataChangeEvent ? textModel.metadata : undefined ); - const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata); if (hasDocumentMetadataChangeEvent) { this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata }); } diff --git a/src/vs/workbench/api/browser/mainThreadTelemetry.ts b/src/vs/workbench/api/browser/mainThreadTelemetry.ts index 1ea9aa30327..58a919fc1e6 100644 --- a/src/vs/workbench/api/browser/mainThreadTelemetry.ts +++ b/src/vs/workbench/api/browser/mainThreadTelemetry.ts @@ -38,7 +38,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet })); } - this._proxy.$initializeTelemetryLevel(this.telemetryLevel); + this._proxy.$initializeTelemetryLevel(this.telemetryLevel, this._productService.enabledTelemetryLevels); } private get telemetryLevel(): TelemetryLevel { diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 29c01dfe5d5..ab0a883d089 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -13,8 +13,9 @@ import { isUndefinedOrNull, isNumber } from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { TreeDataTransferConverter } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -174,12 +175,12 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { readonly hasWillDrop: boolean, private readonly _proxy: ExtHostTreeViewsShape) { } - async handleDrop(dataTransfer: ITreeDataTransfer, targetTreeItem: ITreeItem, token: CancellationToken, + async handleDrop(dataTransfer: IDataTransfer, targetTreeItem: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise { - return this._proxy.$handleDrop(this.treeViewId, await TreeDataTransferConverter.toTreeDataTransferDTO(dataTransfer), targetTreeItem.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); + return this._proxy.$handleDrop(this.treeViewId, await DataTransferConverter.toDataTransferDTO(dataTransfer), targetTreeItem.handle, token, operationUuid, sourceTreeId, sourceTreeItemHandles); } - async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { + async handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { if (!this.hasWillDrop) { return; } @@ -187,7 +188,7 @@ class TreeViewDragAndDropController implements ITreeViewDragAndDropController { if (!additionalTransferItems) { return; } - return TreeDataTransferConverter.toITreeDataTransfer(additionalTransferItems); + return DataTransferConverter.toDataTransfer(additionalTransferItems); } } diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index a7359ef7b2f..3ca501b079b 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -6,6 +6,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { MainThreadWebviews, reviveWebviewContentOptions, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; @@ -16,7 +17,7 @@ import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewE import { WebviewIcons } from 'vs/workbench/contrib/webviewPanel/browser/webviewIconManager'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; import { editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { GroupDirection, GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupLocation, GroupsOrder, IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP, IEditorService, PreferredGroup, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; @@ -89,9 +90,10 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc constructor( context: IExtHostContext, private readonly _mainThreadWebviews: MainThreadWebviews, - @IExtensionService extensionService: IExtensionService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, + @IExtensionService extensionService: IExtensionService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, ) { @@ -166,16 +168,17 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), initData.title, mainThreadShowOptions, reviveWebviewOptions(initData.panelOptions), reviveWebviewContentOptions(initData.webviewOptions), extension); this.addWebviewInput(handle, webview, { serializeBuffersForPostMessage: initData.serializeBuffersForPostMessage }); - /* __GDPR__ - "webviews:createWebviewPanel" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "viewType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('webviews:createWebviewPanel', { + const payload = { extensionId: extension.id.value, viewType - }); + } as const; + + type Classification = { + extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the extension that created the webview panel' }; + viewType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'Id of the webview' }; + }; + + this._telemetryService.publicLog2('webviews:createWebviewPanel', payload); } public $disposeWebview(handle: extHostProtocol.WebviewHandle): void { @@ -229,7 +232,8 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc // of creating all the groups up to 99. const newGroup = this._editorGroupService.findGroup({ location: GroupLocation.LAST }); if (newGroup) { - return this._editorGroupService.addGroup(newGroup, GroupDirection.RIGHT); + const direction = preferredSideBySideGroupDirection(this._configurationService); + return this._editorGroupService.addGroup(newGroup, direction); } } diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 80ef9c70b53..406661573d2 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -290,7 +290,7 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."), items: { type: 'object', - default: { path: '' }, + defaultSnippets: [{ body: { path: '$1' } }], oneOf: [{ properties: { path: { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 86bc738b792..76ee48e81f1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -13,7 +13,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration'; import { score } from 'vs/editor/common/languageSelector'; import * as files from 'vs/platform/files/common/files'; -import { ExtHostContext, MainContext, CandidatePortSource, ExtHostLogLevelServiceShape, TabKind } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, MainContext, CandidatePortSource, ExtHostLogLevelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands'; import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard'; @@ -159,7 +159,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); - const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData)); + const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.remote)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService, extHostFileSystemInfo)); const extHostLanguages = rpcProtocol.set(ExtHostContext.ExtHostLanguages, new ExtHostLanguages(rpcProtocol, extHostDocuments, extHostCommands.converter, uriTransformer)); const extHostLanguageFeatures = rpcProtocol.set(ExtHostContext.ExtHostLanguageFeatures, new ExtHostLanguageFeatures(rpcProtocol, uriTransformer, extHostDocuments, extHostCommands, extHostDiagnostics, extHostLogService, extHostApiDeprecation)); @@ -173,7 +173,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, { remote: initData.remote }, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.remote, extHostWorkspace, extHostLogService, extHostApiDeprecation)); const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); @@ -223,9 +223,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I if (typeof filter.exclusive === 'boolean') { checkProposedApiEnabled(extension, 'documentFiltersExclusive'); } - if (typeof filter.notebookType === 'string') { - checkProposedApiEnabled(extension, 'notebookDocumentSelector'); - } } return selector; }; @@ -498,6 +495,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'inlineCompletions'); return extHostLanguageFeatures.registerInlineCompletionsProvider(extension, checkSelector(selector), provider); }, + registerInlineCompletionItemProviderNew(selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProviderNew): vscode.Disposable { + checkProposedApiEnabled(extension, 'inlineCompletionsNew'); + if (provider.handleDidShowCompletionItem && !isProposedApiEnabled(extension, 'inlineCompletionsAdditions')) { + throw new Error(`When the method "handleDidShowCompletionItem" is implemented on a provider, the usage of the proposed api 'inlineCompletionsAdditions' must be declared!`); + } + return extHostLanguageFeatures.registerInlineCompletionsProviderNew(extension, checkSelector(selector), provider); + }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider); }, @@ -615,6 +619,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostQuickOpen.showWorkspaceFolderPick(options); }, showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { + if (options?.validateInput2) { + checkProposedApiEnabled(extension, 'inputBoxSeverity'); + options.validateInput = options.validateInput2 as any; + } return extHostQuickOpen.showInput(options, token); }, showOpenDialog(options) { @@ -652,9 +660,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostProgress.withProgress(extension, options, task); }, createOutputChannel(name: string, languageId?: string): vscode.OutputChannel { - if (languageId) { - checkProposedApiEnabled(extension, 'outputChannelLanguage'); - } return extHostOutputService.createOutputChannel(name, languageId, extension); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn; preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { @@ -701,7 +706,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostQuickOpen.createQuickPick(extension); }, createInputBox(): vscode.InputBox { - return extHostQuickOpen.createInputBox(extension.identifier); + return extHostQuickOpen.createInputBox(extension); }, get activeColorTheme(): vscode.ColorTheme { return extHostTheming.activeColorTheme; @@ -755,7 +760,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getInlineCompletionItemController(provider: vscode.InlineCompletionItemProvider): vscode.InlineCompletionController { checkProposedApiEnabled(extension, 'inlineCompletions'); return InlineCompletionController.get(provider); - } + }, }; // namespace: workspace @@ -867,6 +872,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillSaveTextDocument: (listener, thisArgs?, disposables?) => { return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, + onWillDropOnTextEditor: (listener, thisArgs?, disposables?) => { + checkProposedApiEnabled(extension, 'textEditorDrop'); + return extHostEditors.onWillDropOnTextEditor(listener, thisArgs, disposables); + }, get notebookDocuments(): vscode.NotebookDocument[] { return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); }, @@ -882,6 +891,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, + onDidSaveNotebookDocument(listener, thisArg, disposables) { + checkProposedApiEnabled(extension, 'notebookDocumentEvents'); + return extHostNotebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); + }, + onDidChangeNotebookDocument(listener, thisArg, disposables) { + checkProposedApiEnabled(extension, 'notebookDocumentEvents'); + return extHostNotebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); + }, get onDidOpenNotebookDocument(): Event { return extHostNotebook.onDidOpenNotebookDocument; }, @@ -1203,6 +1220,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineValueVariableLookup: extHostTypes.InlineValueVariableLookup, InlineValueEvaluatableExpression: extHostTypes.InlineValueEvaluatableExpression, InlineCompletionTriggerKind: extHostTypes.InlineCompletionTriggerKind, + InlineCompletionTriggerKindNew: extHostTypes.InlineCompletionTriggerKindNew, EventEmitter: Emitter, ExtensionKind: extHostTypes.ExtensionKind, ExtensionMode: extHostTypes.ExtensionMode, @@ -1216,7 +1234,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, InlineCompletionItem: extHostTypes.InlineSuggestion, + InlineCompletionItemNew: extHostTypes.InlineSuggestionNew, InlineCompletionList: extHostTypes.InlineSuggestions, + InlineCompletionListNew: extHostTypes.InlineSuggestionsNew, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, @@ -1249,7 +1269,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SymbolInformation: extHostTypes.SymbolInformation, SymbolKind: extHostTypes.SymbolKind, SymbolTag: extHostTypes.SymbolTag, - TabKind: TabKind, Task: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, TaskPanelKind: extHostTypes.TaskPanelKind, @@ -1303,8 +1322,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TestTag: extHostTypes.TestTag, TestRunProfileKind: extHostTypes.TestRunProfileKind, TextSearchCompleteMessageType: TextSearchCompleteMessageType, - TreeDataTransfer: extHostTypes.TreeDataTransfer, - TreeDataTransferItem: extHostTypes.TreeDataTransferItem, + DataTransfer: extHostTypes.DataTransfer, + DataTransferItem: extHostTypes.DataTransferItem, CoveredCount: extHostTypes.CoveredCount, FileCoverage: extHostTypes.FileCoverage, StatementCoverage: extHostTypes.StatementCoverage, @@ -1313,6 +1332,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I WorkspaceTrustState: extHostTypes.WorkspaceTrustState, LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, QuickPickItemKind: extHostTypes.QuickPickItemKind, + InputBoxValidationSeverity: extHostTypes.InputBoxValidationSeverity, + TextTabInput: extHostTypes.TextTabInput, + TextDiffTabInput: extHostTypes.TextDiffTabInput, + CustomEditorTabInput: extHostTypes.CustomEditorTabInput, + NotebookEditorTabInput: extHostTypes.NotebookEditorTabInput, + NotebookDiffEditorTabInput: extHostTypes.NotebookDiffEditorTabInput }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 411c78fc196..74e9add6a55 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -44,7 +44,7 @@ import { ThemeColor, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ProvidedPortAttributes, TunnelCreationOptions, TunnelOptions, TunnelPrivacyId, TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; -import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { SaveReason } from 'vs/workbench/common/editor'; import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; @@ -114,24 +114,25 @@ export interface CommentChanges { readonly timestamp?: string; } -export type CommentThreadChanges = Partial<{ - range: IRange; +export type CommentThreadChanges = Partial<{ + range: T; label: string; contextValue: string | null; comments: CommentChanges[]; collapseState: languages.CommentThreadCollapsibleState; canReply: boolean; + state: languages.CommentThreadState; }>; export interface MainThreadCommentsShape extends IDisposable { $registerCommentController(handle: number, id: string, label: string): void; $unregisterCommentController(handle: number): void; $updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void; - $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange, extensionId: ExtensionIdentifier): languages.CommentThread | undefined; + $createCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, range: IRange | ICellRange, extensionId: ExtensionIdentifier): languages.CommentThread | undefined; $updateCommentThread(handle: number, commentThreadHandle: number, threadId: string, resource: UriComponents, changes: CommentThreadChanges): void; $deleteCommentThread(handle: number, commentThreadHandle: number): void; $updateCommentingRanges(handle: number): void; - $onDidCommentThreadsChange(handle: number, event: languages.CommentThreadChangedEvent): void; + $onDidCommentThreadsChange(handle: number, event: languages.CommentThreadChangedEvent): void; } export interface MainThreadAuthenticationShape extends IDisposable { @@ -609,10 +610,55 @@ export interface ExtHostEditorInsetsShape { //#region --- tabs model +export const enum TabInputKind { + UnknownInput, + TextInput, + TextDiffInput, + NotebookInput, + NotebookDiffInput, + CustomEditorInput +} + +export interface UnknownInputDto { + kind: TabInputKind.UnknownInput; +} + +export interface TextInputDto { + kind: TabInputKind.TextInput; + uri: UriComponents; +} + +export interface TextDiffInputDto { + kind: TabInputKind.TextDiffInput; + original: UriComponents; + modified: UriComponents; +} + +export interface NotebookInputDto { + kind: TabInputKind.NotebookInput; + notebookType: string; + uri: UriComponents; +} + +export interface NotebookDiffInputDto { + kind: TabInputKind.NotebookDiffInput; + notebookType: string; + original: UriComponents; + modified: UriComponents; +} + +export interface CustomInputDto { + kind: TabInputKind.CustomEditorInput; + viewType: string; + uri: UriComponents; +} + +export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto; + export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc - $moveTab(tab: IEditorTabDto, index: number, viewColumn: EditorGroupColumn): void; - $closeTab(tab: IEditorTabDto): Promise; + $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; + $closeTab(tabIds: string[], preserveFocus?: boolean): Promise; } export interface IEditorTabGroupDto { @@ -620,32 +666,28 @@ export interface IEditorTabGroupDto { viewColumn: EditorGroupColumn; // Decided not to go with simple index here due to opening and closing causing index shifts // This allows us to patch the model without having to do full rebuilds - activeTab: IEditorTabDto | undefined; tabs: IEditorTabDto[]; groupId: number; } -export enum TabKind { - Singular = 0, - Diff = 1, - SidebySide = 2, - Other = 3 -} - export interface IEditorTabDto { - viewColumn: EditorGroupColumn; + id: string; label: string; - resource?: UriComponents; + input: AnyInputDto; editorId?: string; isActive: boolean; isPinned: boolean; + isPreview: boolean; isDirty: boolean; - kind: TabKind; - additionalResourcesAndViewIds: { resource?: UriComponents; viewId?: string }[]; } export interface IExtHostEditorTabsShape { + // Accepts a whole new model $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void; + // Only when group property changes (not the tabs inside) + $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto): void; + // Only when tab property changes + $acceptTabUpdate(groupId: number, tabDto: IEditorTabDto): void; } //#endregion @@ -1261,6 +1303,7 @@ export interface ISelectionChangeEvent { export interface ExtHostEditorsShape { $acceptEditorPropertiesChanged(id: string, props: IEditorPropertiesChangeData): void; $acceptEditorPositionData(data: ITextEditorPositionData): void; + $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise; } export interface IDocumentsAndEditorsDelta { @@ -1277,8 +1320,8 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise; - $handleDrop(destinationViewId: string, treeDataTransfer: TreeDataTransferDTO, newParentTreeItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; - $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; + $handleDrop(destinationViewId: string, treeDataTransfer: DataTransferDTO, newParentTreeItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; + $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; @@ -1339,7 +1382,10 @@ export interface ExtHostSearchShape { export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; - $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; + /** + * Returns `null` if no resolver for `remoteAuthority` is found. + */ + $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; $extensionTestsExecute(): Promise; $extensionTestsExit(code: number): Promise; @@ -1666,7 +1712,7 @@ export interface ExtHostLanguageFeaturesShape { export interface ExtHostQuickOpenShape { $onItemSelected(handle: number): void; - $validateInput(input: string): Promise; + $validateInput(input: string): Promise; $onDidChangeActive(sessionId: number, handles: number[]): void; $onDidChangeSelection(sessionId: number, handles: number[]): void; $onDidAccept(sessionId: number): void; @@ -1677,7 +1723,7 @@ export interface ExtHostQuickOpenShape { } export interface ExtHostTelemetryShape { - $initializeTelemetryLevel(level: TelemetryLevel): void; + $initializeTelemetryLevel(level: TelemetryLevel, productConfig?: { usage: boolean; error: boolean }): void; $onDidChangeTelemetryLevel(level: TelemetryLevel): void; } @@ -2005,7 +2051,7 @@ export type NotebookCellsChangedEventDto = { }; export interface ExtHostNotebookDocumentsShape { - $acceptModelChanged(uriComponents: UriComponents, event: SerializableObjectWithBuffers, isDirty: boolean): void; + $acceptModelChanged(uriComponents: UriComponents, event: SerializableObjectWithBuffers, isDirty: boolean, newMetadata?: notebookCommon.NotebookDocumentMetadata): void; $acceptDirtyStateChanged(uriComponents: UriComponents, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; diff --git a/src/vs/workbench/api/common/extHostApiDeprecationService.ts b/src/vs/workbench/api/common/extHostApiDeprecationService.ts index b39792a54bf..a407b241c33 100644 --- a/src/vs/workbench/api/common/extHostApiDeprecationService.ts +++ b/src/vs/workbench/api/common/extHostApiDeprecationService.ts @@ -47,8 +47,8 @@ export class ExtHostApiDeprecationService implements IExtHostApiDeprecationServi apiId: string; }; type DeprecationTelemetryMeta = { - extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; - apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + extensionId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the extension that is using the deprecated API' }; + apiId: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; owner: 'mjbvz'; comment: 'The id of the deprecated API' }; }; this._telemetryShape.$publicLog2('extHostDeprecatedApiUsage', { extensionId: extension.identifier.value, diff --git a/src/vs/workbench/api/common/extHostCodeInsets.ts b/src/vs/workbench/api/common/extHostCodeInsets.ts index 9dbd644d15c..e91a8eae63c 100644 --- a/src/vs/workbench/api/common/extHostCodeInsets.ts +++ b/src/vs/workbench/api/common/extHostCodeInsets.ts @@ -8,7 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostTextEditor } from 'vs/workbench/api/common/extHostTextEditor'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/common/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewRemoteInfo } from 'vs/workbench/common/webview'; import type * as vscode from 'vscode'; import { ExtHostEditorInsetsShape, MainThreadEditorInsetsShape } from './extHost.protocol'; @@ -21,7 +21,7 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { constructor( private readonly _proxy: MainThreadEditorInsetsShape, private readonly _editors: ExtHostEditors, - private readonly _initData: WebviewInitData + private readonly _remoteInfo: WebviewRemoteInfo ) { // dispose editor inset whenever the hosting editor goes away @@ -64,7 +64,7 @@ export class ExtHostEditorInsets implements ExtHostEditorInsetsShape { private _options: vscode.WebviewOptions = Object.create(null); asWebviewUri(resource: vscode.Uri): vscode.Uri { - return asWebviewUri(resource, that._initData.remote); + return asWebviewUri(resource, that._remoteInfo); } get cspSource(): string { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index 1f1049f8b4f..2940cff74f4 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -16,6 +16,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import * as extHostTypeConverter from 'vs/workbench/api/common/extHostTypeConverters'; import * as types from 'vs/workbench/api/common/extHostTypes'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; import { ExtHostCommentsShape, IMainContext, MainContext, CommentThreadChanges, CommentChanges } from './extHost.protocol'; import { ExtHostCommands } from './extHostCommands'; @@ -50,7 +51,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } - return commentController; + return commentController.value; } else if (arg && arg.$mid === MarshalledId.CommentThread) { const commentController = this._commentControllers.get(arg.commentControlHandle); @@ -64,7 +65,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } - return commentThread; + return commentThread.value; } else if (arg && arg.$mid === MarshalledId.CommentThreadReply) { const commentController = this._commentControllers.get(arg.thread.commentControlHandle); @@ -79,7 +80,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } return { - thread: commentThread, + thread: commentThread.value, text: arg.text }; } else if (arg && arg.$mid === MarshalledId.CommentNode) { @@ -222,6 +223,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo comments: vscode.Comment[]; collapsibleState: vscode.CommentThreadCollapsibleState; canReply: boolean; + state: vscode.CommentThreadState; }>; class ExtHostCommentThread implements vscode.CommentThread { @@ -325,6 +327,20 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo this._onDidUpdateCommentThread.fire(); } + private _state?: vscode.CommentThreadState; + + get state(): vscode.CommentThreadState { + checkProposedApiEnabled(this.extensionDescription, 'commentsResolvedState'); + return this._state!; + } + + set state(newState: vscode.CommentThreadState) { + checkProposedApiEnabled(this.extensionDescription, 'commentsResolvedState'); + this._state = newState; + this.modifications.state = newState; + this._onDidUpdateCommentThread.fire(); + } + private _localDisposables: types.Disposable[]; private _isDiposed: boolean; @@ -397,6 +413,8 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo set contextValue(value: string | undefined) { that.contextValue = value; }, get label() { return that.label; }, set label(value: string | undefined) { that.label = value; }, + get state() { return that.state; }, + set state(value: vscode.CommentThreadState) { that.state = value; }, dispose: () => { that.dispose(); } @@ -441,6 +459,9 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo if (modified('canReply')) { formattedModifications.canReply = this.canReply; } + if (modified('state')) { + formattedModifications.state = convertToState(this._state); + } this.modifications = {}; proxy.$updateCommentThread( @@ -660,5 +681,17 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return languages.CommentThreadCollapsibleState.Collapsed; } + function convertToState(kind: vscode.CommentThreadState | undefined): languages.CommentThreadState { + if (kind !== undefined) { + switch (kind) { + case types.CommentThreadState.Unresolved: + return languages.CommentThreadState.Unresolved; + case types.CommentThreadState.Resolved: + return languages.CommentThreadState.Resolved; + } + } + return languages.CommentThreadState.Unresolved; + } + return new ExtHostCommentsImpl(); } diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index eca1651b33b..3a45d62f28f 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -277,7 +277,7 @@ export class ExtHostConfigProvider { mixin(result, config, false); } - return Object.freeze(result); + return Object.freeze(result); } private _toReadonlyValue(result: any): any { diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 26314660114..2b1854dbe8a 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -11,7 +11,7 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, TextDiffTabInput, NotebookDiffEditorTabInput, TextTabInput, NotebookEditorTabInput, CustomEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; @@ -953,14 +953,13 @@ export class ExtHostVariableResolverService extends AbstractVariableResolverServ if (activeEditor) { return activeEditor.document.uri; } - const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; + const activeTab = editorTabs.tabGroups.groups.find(group => group.isActive)?.activeTab; if (activeTab !== undefined) { // Resolve a resource from the tab - const asSideBySideResource = activeTab.resource as { primary?: URI; secondary?: URI } | undefined; - if (asSideBySideResource && (asSideBySideResource.primary || asSideBySideResource.secondary)) { - return asSideBySideResource.primary ?? asSideBySideResource.secondary; - } else { - return activeTab.resource as URI | undefined; + if (activeTab.input instanceof TextDiffTabInput || activeTab.input instanceof NotebookDiffEditorTabInput) { + return activeTab.input.modified; + } else if (activeTab.input instanceof TextTabInput || activeTab.input instanceof NotebookEditorTabInput || activeTab.input instanceof CustomEditorTabInput) { + return activeTab.input.uri; } } } diff --git a/src/vs/workbench/api/common/extHostDiagnostics.ts b/src/vs/workbench/api/common/extHostDiagnostics.ts index bacfe4bdd80..a81ce5faf65 100644 --- a/src/vs/workbench/api/common/extHostDiagnostics.ts +++ b/src/vs/workbench/api/common/extHostDiagnostics.ts @@ -191,7 +191,7 @@ export class DiagnosticCollection implements vscode.DiagnosticCollection { this._checkDisposed(); const result = this.#data.get(uri); if (Array.isArray(result)) { - return >Object.freeze(result.slice(0)); + return Object.freeze(result.slice(0)); } return []; } diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 4b43d2f917a..c3d05a440e8 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -106,7 +106,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic const { document, reason } = stubEvent; const { version } = document; - const event = Object.freeze({ + const event = Object.freeze({ document, reason, waitUntil(p: Promise) { diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index 5230e595d9b..f94c1479fac 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -5,110 +5,347 @@ import type * as vscode from 'vscode'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; -import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabKind } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ViewColumn } from 'vs/workbench/api/common/extHostTypes'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TextDiffTabInput, TextTabInput, ViewColumn } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -export interface IEditorTab { - label: string; - viewColumn: ViewColumn; - resource: vscode.Uri | undefined; - viewId: string | undefined; - isActive: boolean; - isPinned: boolean; - kind: TabKind; - isDirty: boolean; - additionalResourcesAndViewIds: { resource: vscode.Uri | undefined; viewId: string | undefined }[]; - move(index: number, viewColumn: ViewColumn): Promise; - close(): Promise; -} - -export interface IEditorTabGroup { - isActive: boolean; - viewColumn: ViewColumn; - activeTab: IEditorTab | undefined; - tabs: IEditorTab[]; -} - -export interface IEditorTabGroups { - all: IEditorTabGroup[]; - onDidChangeTabGroup: Event; -} export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { readonly _serviceBrand: undefined; - tabGroups: IEditorTabGroups; + tabGroups: vscode.TabGroups; } export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); +type AnyTabInput = TextTabInput | TextDiffTabInput; + +class ExtHostEditorTab { + private _apiObject: vscode.Tab | undefined; + private _dto!: IEditorTabDto; + private _input: AnyTabInput | undefined; + private _parentGroup: ExtHostEditorTabGroup; + private readonly _activeTabIdGetter: () => string; + + constructor(dto: IEditorTabDto, parentGroup: ExtHostEditorTabGroup, activeTabIdGetter: () => string) { + this._activeTabIdGetter = activeTabIdGetter; + this._parentGroup = parentGroup; + this.acceptDtoUpdate(dto); + } + + get apiObject(): vscode.Tab { + // Don't want to lose reference to parent `this` in the getters + const that = this; + if (!this._apiObject) { + const obj: vscode.Tab = { + get isActive() { + // We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required + return that._dto.id === that._activeTabIdGetter(); + }, + get label() { + return that._dto.label; + }, + get input() { + return that._input; + }, + get isDirty() { + return that._dto.isDirty; + }, + get isPinned() { + return that._dto.isDirty; + }, + get isPreview() { + return that._dto.isPreview; + }, + get parentGroup() { + return that._parentGroup.apiObject; + } + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + get tabId(): string { + return this._dto.id; + } + + acceptDtoUpdate(dto: IEditorTabDto) { + this._dto = dto; + this._input = this._initInput(); + } + + private _initInput() { + switch (this._dto.input.kind) { + case TabInputKind.TextInput: + return new TextTabInput(URI.revive(this._dto.input.uri)); + case TabInputKind.TextDiffInput: + return new TextDiffTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified)); + case TabInputKind.CustomEditorInput: + return new CustomEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.viewType); + case TabInputKind.NotebookInput: + return new NotebookEditorTabInput(URI.revive(this._dto.input.uri), this._dto.input.notebookType); + case TabInputKind.NotebookDiffInput: + return new NotebookDiffEditorTabInput(URI.revive(this._dto.input.original), URI.revive(this._dto.input.modified), this._dto.input.notebookType); + default: + return undefined; + } + } +} + +class ExtHostEditorTabGroup { + + private _apiObject: vscode.TabGroup | undefined; + private _dto: IEditorTabGroupDto; + private _tabs: ExtHostEditorTab[] = []; + private _activeTabId: string = ''; + private _activeGroupIdGetter: () => number | undefined; + + constructor(dto: IEditorTabGroupDto, proxy: MainThreadEditorTabsShape, activeGroupIdGetter: () => number | undefined) { + this._dto = dto; + this._activeGroupIdGetter = activeGroupIdGetter; + // Construct all tabs from the given dto + for (const tabDto of dto.tabs) { + if (tabDto.isActive) { + this._activeTabId = tabDto.id; + } + this._tabs.push(new ExtHostEditorTab(tabDto, this, () => this.activeTabId())); + } + } + + get apiObject(): vscode.TabGroup { + // Don't want to lose reference to parent `this` in the getters + const that = this; + if (!this._apiObject) { + const obj: vscode.TabGroup = { + get isActive() { + // We use a getter function here to always ensure at most 1 active group and prevent iteration for being required + return that._dto.groupId === that._activeGroupIdGetter(); + }, + get viewColumn() { + return typeConverters.ViewColumn.to(that._dto.viewColumn); + }, + get activeTab() { + return that._tabs.find(tab => tab.tabId === that._activeTabId)?.apiObject; + }, + get tabs() { + return Object.freeze(that._tabs.map(tab => tab.apiObject)); + } + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + get groupId(): number { + return this._dto.groupId; + } + + get tabs(): ExtHostEditorTab[] { + return this._tabs; + } + + acceptGroupDtoUpdate(dto: IEditorTabGroupDto) { + this._dto = dto; + } + + acceptTabDtoUpdate(dto: IEditorTabDto) { + const tab = this._tabs.find(extHostTab => extHostTab.tabId === dto.id); + if (!tab) { + throw new Error('INVALID tab'); + } + if (dto.isActive) { + this._activeTabId = dto.id; + } + tab.acceptDtoUpdate(dto); + return tab; + } + + // Not a getter since it must be a function to be used as a callback for the tabs + activeTabId(): string { + return this._activeTabId; + } +} + export class ExtHostEditorTabs implements IExtHostEditorTabs { readonly _serviceBrand: undefined; + private readonly _proxy: MainThreadEditorTabsShape; - + private readonly _onDidChangeTab = new Emitter(); private readonly _onDidChangeTabGroup = new Emitter(); - readonly onDidChangeTabGroup: Event = this._onDidChangeTabGroup.event; + private readonly _onDidChangeActiveTabGroup = new Emitter(); - private _tabGroups: IEditorTabGroups = { - all: [], - onDidChangeTabGroup: this._onDidChangeTabGroup.event - }; + private _activeGroupId: number | undefined; + + private _extHostTabGroups: ExtHostEditorTabGroup[] = []; + + private _apiObject: vscode.TabGroups | undefined; constructor(@IExtHostRpcService extHostRpc: IExtHostRpcService) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadEditorTabs); } - get tabGroups(): IEditorTabGroups { - return this._tabGroups; + get tabGroups(): vscode.TabGroups { + if (!this._apiObject) { + const that = this; + const obj: vscode.TabGroups = { + // never changes -> simple value + onDidChangeTabGroup: that._onDidChangeTabGroup.event, + onDidChangeActiveTabGroup: that._onDidChangeActiveTabGroup.event, + onDidChangeTab: that._onDidChangeTab.event, + // dynamic -> getters + get groups() { + return Object.freeze(that._extHostTabGroups.map(group => group.apiObject)); + }, + get activeTabGroup() { + const activeTabGroupId = that._activeGroupId; + if (activeTabGroupId === undefined) { + return undefined; + } + return that._extHostTabGroups.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject; + }, + close: async (tab: vscode.Tab | vscode.Tab[], preserveFocus?: boolean) => { + const tabs = Array.isArray(tab) ? tab : [tab]; + const extHostTabIds: string[] = []; + for (const tab of tabs) { + const extHostTab = this._findExtHostTabFromApi(tab); + if (!extHostTab) { + throw new Error('Tab close: Invalid tab not found!'); + } + extHostTabIds.push(extHostTab.tabId); + } + this._proxy.$closeTab(extHostTabIds, preserveFocus); + return; + }, + move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { + const extHostTab = this._findExtHostTabFromApi(tab); + if (!extHostTab) { + throw new Error('Invalid tab'); + } + this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); + return; + } + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { + for (const group of this._extHostTabGroups) { + for (const tab of group.tabs) { + if (tab.apiObject === apiTab) { + return tab; + } + } + } + return; } $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { - // Clears the tab groups array - this._tabGroups.all.length = 0; - for (const group of tabGroups) { - let activeTab: IEditorTab | undefined; - const tabs = group.tabs.map(tab => { - const extHostTab = this.createExtHostTabObject(tab); - if (tab.isActive) { - activeTab = extHostTab; - } - return extHostTab; - }); - this._tabGroups.all.push(Object.freeze({ - isActive: group.isActive, - viewColumn: typeConverters.ViewColumn.to(group.viewColumn), - activeTab, - tabs - })); + + this._extHostTabGroups = tabGroups.map(tabGroup => { + const group = new ExtHostEditorTabGroup(tabGroup, this._proxy, () => this._activeGroupId); + return group; + }); + + // Set the active tab group id + const activeTabGroupId = tabGroups.find(group => group.isActive === true)?.groupId; + if (this._activeGroupId !== activeTabGroupId) { + this._activeGroupId = activeTabGroupId; + this._onDidChangeActiveTabGroup.fire(this.tabGroups.activeTabGroup); } this._onDidChangeTabGroup.fire(); } - private createExtHostTabObject(tabDto: IEditorTabDto): IEditorTab { - return Object.freeze({ - label: tabDto.label, - viewColumn: typeConverters.ViewColumn.to(tabDto.viewColumn), - resource: URI.revive(tabDto.resource), - additionalResourcesAndViewIds: tabDto.additionalResourcesAndViewIds.map(({ resource, viewId }) => ({ resource: URI.revive(resource), viewId })), - viewId: tabDto.editorId, - isActive: tabDto.isActive, - kind: tabDto.kind, - isDirty: tabDto.isDirty, - isPinned: tabDto.isPinned, - move: async (index: number, viewColumn: ViewColumn) => { - this._proxy.$moveTab(tabDto, index, typeConverters.ViewColumn.from(viewColumn)); - // TODO: Need an on did change tab event at the group level - // await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000); - return; - }, - close: async () => { - await this._proxy.$closeTab(tabDto); - // TODO: Need an on did change tab event at the group level - // await raceTimeout(Event.toPromise(this._onDidChangeTabs.event), 1000); - return; + $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { + const group = this._extHostTabGroups.find(group => group.groupId === groupDto.groupId); + if (!group) { + throw new Error('Update Group IPC call received before group creation.'); + } + group.acceptGroupDtoUpdate(groupDto); + if (groupDto.isActive) { + const oldActiveGroupId = this._activeGroupId; + this._activeGroupId = groupDto.groupId; + if (oldActiveGroupId !== this._activeGroupId) { + this._onDidChangeActiveTabGroup.fire(group.apiObject); } - }); + } + this._onDidChangeTabGroup.fire(); } + + $acceptTabUpdate(groupId: number, tabDto: IEditorTabDto) { + const group = this._extHostTabGroups.find(group => group.groupId === groupId); + if (!group) { + throw new Error('Update Tabs IPC call received before group creation.'); + } + const tab = group.acceptTabDtoUpdate(tabDto); + this._onDidChangeTab.fire(tab.apiObject); + } + + /** + * Compares two groups determining if they're the same or different + * @param group1 The first group to compare + * @param group2 The second group to compare + * @returns True if different, false otherwise + */ + // private groupDiff(group1: IEditorTabGroup | undefined, group2: IEditorTabGroup | undefined): boolean { + // if (group1 === group2) { + // return false; + // } + // // They would be reference equal if both undefined so one is undefined and one isn't hence different + // if (!group1 || !group2) { + // return true; + // } + // if (group1.isActive !== group2.isActive + // || group1.viewColumn !== group2.viewColumn + // || group1.tabs.length !== group2.tabs.length + // ) { + // return true; + // } + // for (let i = 0; i < group1.tabs.length; i++) { + // if (this.tabDiff(group1.tabs[i], group2.tabs[i])) { + // return true; + // } + // } + // return false; + // } + + /** + * Compares two tabs determining if they're the same or different + * @param tab1 The first tab to compare + * @param tab2 The second tab to compare + * @returns True if different, false otherwise + */ + // private tabDiff(tab1: IEditorTab | undefined, tab2: IEditorTab | undefined): boolean { + // if (tab1 === tab2) { + // return false; + // } + // // They would be reference equal if both undefined so one is undefined and one isn't therefore they're different + // if (!tab1 || !tab2) { + // return true; + // } + // if (tab1.label !== tab2.label + // || tab1.viewColumn !== tab2.viewColumn + // || tab1.resource?.toString() !== tab2.resource?.toString() + // || tab1.viewType !== tab2.viewType + // || tab1.isActive !== tab2.isActive + // || tab1.isPinned !== tab2.isPinned + // || tab1.isDirty !== tab2.isDirty + // || tab1.additionalResourcesAndViewTypes.length !== tab2.additionalResourcesAndViewTypes.length + // ) { + // return true; + // } + // for (let i = 0; i < tab1.additionalResourcesAndViewTypes.length; i++) { + // const tab1Resource = tab1.additionalResourcesAndViewTypes[i].resource; + // const tab2Resource = tab2.additionalResourcesAndViewTypes[i].resource; + // const tab1viewType = tab1.additionalResourcesAndViewTypes[i].viewType; + // const tab2viewType = tab2.additionalResourcesAndViewTypes[i].viewType; + // if (tab1Resource?.toString() !== tab2Resource?.toString() || tab1viewType !== tab2viewType) { + // return true; + // } + // } + // return false; + // } } diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 59ff5889c3f..a0a16865ab2 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -10,8 +10,7 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; - -const NO_OP_VOID_PROMISE = Promise.resolve(undefined); +import { Barrier } from 'vs/base/common/async'; /** * Represents the source code (module) of an extension. @@ -166,14 +165,11 @@ type ActivationIdAndReason = { id: ExtensionIdentifier; reason: ExtensionActivat export class ExtensionsActivator implements IDisposable { - private _isDisposed: boolean; - private readonly _registry: ExtensionDescriptionRegistry; private readonly _resolvedExtensionsSet: Set; - private readonly _hostExtensionsMap: Map; + private readonly _externalExtensionsMap: Map; private readonly _host: IExtensionsActivatorHost; - private readonly _activatingExtensions: Map>; - private readonly _activatedExtensions: Map; + private readonly _operations: Map; /** * A map of already activated events to speed things up if the same activation event is triggered multiple times. */ @@ -182,130 +178,115 @@ export class ExtensionsActivator implements IDisposable { constructor( registry: ExtensionDescriptionRegistry, resolvedExtensions: ExtensionIdentifier[], - hostExtensions: ExtensionIdentifier[], + externalExtensions: ExtensionIdentifier[], host: IExtensionsActivatorHost, @ILogService private readonly _logService: ILogService ) { - this._isDisposed = false; this._registry = registry; this._resolvedExtensionsSet = new Set(); resolvedExtensions.forEach((extensionId) => this._resolvedExtensionsSet.add(ExtensionIdentifier.toKey(extensionId))); - this._hostExtensionsMap = new Map(); - hostExtensions.forEach((extensionId) => this._hostExtensionsMap.set(ExtensionIdentifier.toKey(extensionId), extensionId)); + this._externalExtensionsMap = new Map(); + externalExtensions.forEach((extensionId) => this._externalExtensionsMap.set(ExtensionIdentifier.toKey(extensionId), extensionId)); this._host = host; - this._activatingExtensions = new Map>(); - this._activatedExtensions = new Map(); + this._operations = new Map(); this._alreadyActivatedEvents = Object.create(null); } public dispose(): void { - this._isDisposed = true; + for (const [_, op] of this._operations) { + op.dispose(); + } } public isActivated(extensionId: ExtensionIdentifier): boolean { - const extensionKey = ExtensionIdentifier.toKey(extensionId); - - return this._activatedExtensions.has(extensionKey); + const op = this._operations.get(ExtensionIdentifier.toKey(extensionId)); + return Boolean(op && op.value); } public getActivatedExtension(extensionId: ExtensionIdentifier): ActivatedExtension { - const extensionKey = ExtensionIdentifier.toKey(extensionId); - - const activatedExtension = this._activatedExtensions.get(extensionKey); - if (!activatedExtension) { - throw new Error('Extension `' + extensionId.value + '` is not known or not activated'); + const op = this._operations.get(ExtensionIdentifier.toKey(extensionId)); + if (!op || !op.value) { + throw new Error(`Extension '${extensionId.value}' is not known or not activated`); } - return activatedExtension; + return op.value; } - public activateByEvent(activationEvent: string, startup: boolean): Promise { + public async activateByEvent(activationEvent: string, startup: boolean): Promise { if (this._alreadyActivatedEvents[activationEvent]) { - return NO_OP_VOID_PROMISE; + return; } + const activateExtensions = this._registry.getExtensionDescriptionsForActivationEvent(activationEvent); - return this._activateExtensions(activateExtensions.map(e => ({ + await this._activateExtensions(activateExtensions.map(e => ({ id: e.identifier, reason: { startup, extensionId: e.identifier, activationEvent } - }))).then(() => { - this._alreadyActivatedEvents[activationEvent] = true; - }); + }))); + + this._alreadyActivatedEvents[activationEvent] = true; } public activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { const desc = this._registry.getExtensionDescription(extensionId); if (!desc) { - throw new Error('Extension `' + extensionId + '` is not known'); + throw new Error(`Extension '${extensionId}' is not known`); } + return this._activateExtensions([{ id: desc.identifier, reason }]); + } - return this._activateExtensions([{ - id: desc.identifier, - reason - }]); + private async _activateExtensions(extensions: ActivationIdAndReason[]): Promise { + const operations = extensions + .filter((p) => !this.isActivated(p.id)) + .map(ext => this._handleActivationRequest(ext)); + await Promise.all(operations.map(op => op.wait())); } /** * Handle semantics related to dependencies for `currentExtension`. - * semantics: `redExtensions` must wait for `greenExtensions`. + * We don't need to worry about dependency loops because they are handled by the registry. */ - private _handleActivateRequest(currentActivation: ActivationIdAndReason, greenExtensions: { [id: string]: ActivationIdAndReason }, redExtensions: ActivationIdAndReason[]): void { - if (this._hostExtensionsMap.has(ExtensionIdentifier.toKey(currentActivation.id))) { - greenExtensions[ExtensionIdentifier.toKey(currentActivation.id)] = currentActivation; - return; + private _handleActivationRequest(currentActivation: ActivationIdAndReason): ActivationOperation { + if (this._operations.has(ExtensionIdentifier.toKey(currentActivation.id))) { + return this._operations.get(ExtensionIdentifier.toKey(currentActivation.id))!; + } + + if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(currentActivation.id))) { + return this._createAndSaveOperation(currentActivation, null, [], null); } const currentExtension = this._registry.getExtensionDescription(currentActivation.id); if (!currentExtension) { // Error condition 0: unknown extension const error = new Error(`Cannot activate unknown extension '${currentActivation.id.value}'`); + const result = this._createAndSaveOperation(currentActivation, null, [], new FailedExtension(error)); this._host.onExtensionActivationError( currentActivation.id, error, new MissingExtensionDependency(currentActivation.id.value) ); - this._activatedExtensions.set(ExtensionIdentifier.toKey(currentActivation.id), new FailedExtension(error)); - return; + return result; } + const deps: ActivationOperation[] = []; const depIds = (typeof currentExtension.extensionDependencies === 'undefined' ? [] : currentExtension.extensionDependencies); - let currentExtensionGetsGreenLight = true; - - for (let j = 0, lenJ = depIds.length; j < lenJ; j++) { - const depId = depIds[j]; + for (const depId of depIds) { if (this._resolvedExtensionsSet.has(ExtensionIdentifier.toKey(depId))) { // This dependency is already resolved continue; } - const dep = this._activatedExtensions.get(ExtensionIdentifier.toKey(depId)); - if (dep && !dep.activationFailed) { - // the dependency is already activated OK + const dep = this._operations.get(ExtensionIdentifier.toKey(depId)); + if (dep) { + deps.push(dep); continue; } - if (dep && dep.activationFailed) { - // Error condition 2: a dependency has already failed activation - const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value; - const depDesc = this._registry.getExtensionDescription(depId); - const depFriendlyName = (depDesc ? depDesc.displayName || depId : depId); - const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because its dependency '${depFriendlyName}' failed to activate`); - (error).detail = dep.activationFailedError; - this._host.onExtensionActivationError( - currentExtension.identifier, - error, - null - ); - this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error)); - return; - } - - if (this._hostExtensionsMap.has(ExtensionIdentifier.toKey(depId))) { + if (this._externalExtensionsMap.has(ExtensionIdentifier.toKey(depId))) { // must first wait for the dependency to activate - currentExtensionGetsGreenLight = false; - greenExtensions[ExtensionIdentifier.toKey(depId)] = { - id: this._hostExtensionsMap.get(ExtensionIdentifier.toKey(depId))!, + deps.push(this._handleActivationRequest({ + id: this._externalExtensionsMap.get(ExtensionIdentifier.toKey(depId))!, reason: currentActivation.reason - }; + })); continue; } @@ -317,118 +298,140 @@ export class ExtensionsActivator implements IDisposable { } // must first wait for the dependency to activate - currentExtensionGetsGreenLight = false; - greenExtensions[ExtensionIdentifier.toKey(depId)] = { + deps.push(this._handleActivationRequest({ id: depDesc.identifier, reason: currentActivation.reason - }; + })); continue; } // Error condition 1: unknown dependency const currentExtensionFriendlyName = currentExtension.displayName || currentExtension.identifier.value; const error = new Error(`Cannot activate the '${currentExtensionFriendlyName}' extension because it depends on unknown extension '${depId}'`); + const result = this._createAndSaveOperation(currentActivation, currentExtension.displayName, [], new FailedExtension(error)); this._host.onExtensionActivationError( currentExtension.identifier, error, new MissingExtensionDependency(depId) ); - this._activatedExtensions.set(ExtensionIdentifier.toKey(currentExtension.identifier), new FailedExtension(error)); + return result; + } + + return this._createAndSaveOperation(currentActivation, currentExtension.displayName, deps, null); + } + + private _createAndSaveOperation(activation: ActivationIdAndReason, displayName: string | null | undefined, deps: ActivationOperation[], value: ActivatedExtension | null): ActivationOperation { + const operation = new ActivationOperation(activation.id, displayName, activation.reason, deps, value, this._host, this._logService); + this._operations.set(ExtensionIdentifier.toKey(activation.id), operation); + return operation; + } +} + +class ActivationOperation { + + private readonly _barrier = new Barrier(); + private _isDisposed = false; + + public get value(): ActivatedExtension | null { + return this._value; + } + + public get friendlyName(): string { + return this._displayName || this._id.value; + } + + constructor( + private readonly _id: ExtensionIdentifier, + private readonly _displayName: string | null | undefined, + private readonly _reason: ExtensionActivationReason, + private readonly _deps: ActivationOperation[], + private _value: ActivatedExtension | null, + private readonly _host: IExtensionsActivatorHost, + @ILogService private readonly _logService: ILogService + ) { + this._initialize(); + } + + public dispose(): void { + this._isDisposed = true; + } + + public wait() { + return this._barrier.wait(); + } + + private async _initialize(): Promise { + await this._waitForDepsThenActivate(); + this._barrier.open(); + } + + private async _waitForDepsThenActivate(): Promise { + if (this._value) { + // this operation is already finished return; } - if (currentExtensionGetsGreenLight) { - greenExtensions[ExtensionIdentifier.toKey(currentExtension.identifier)] = currentActivation; - } else { - redExtensions.push(currentActivation); - } - } + while (this._deps.length > 0) { + // remove completed deps + for (let i = 0; i < this._deps.length; i++) { + const dep = this._deps[i]; - private _activateExtensions(extensions: ActivationIdAndReason[]): Promise { - if (extensions.length === 0) { - return Promise.resolve(undefined); - } + if (dep.value && !dep.value.activationFailed) { + // the dependency is already activated OK + this._deps.splice(i, 1); + i--; + continue; + } - extensions = extensions.filter((p) => !this._activatedExtensions.has(ExtensionIdentifier.toKey(p.id))); - if (extensions.length === 0) { - return Promise.resolve(undefined); - } + if (dep.value && dep.value.activationFailed) { + // Error condition 2: a dependency has already failed activation + const error = new Error(`Cannot activate the '${this.friendlyName}' extension because its dependency '${dep.friendlyName}' failed to activate`); + (error).detail = dep.value.activationFailedError; + this._value = new FailedExtension(error); + this._host.onExtensionActivationError(this._id, error, null); + return; + } + } - const greenMap: { [id: string]: ActivationIdAndReason } = Object.create(null), - red: ActivationIdAndReason[] = []; - - for (let i = 0, len = extensions.length; i < len; i++) { - this._handleActivateRequest(extensions[i], greenMap, red); - } - - // Make sure no red is also green - for (let i = 0, len = red.length; i < len; i++) { - const redExtensionKey = ExtensionIdentifier.toKey(red[i].id); - if (greenMap[redExtensionKey]) { - delete greenMap[redExtensionKey]; + if (this._deps.length > 0) { + // wait for one dependency + await Promise.race(this._deps.map(dep => dep.wait())); } } - const green = Object.keys(greenMap).map(id => greenMap[id]); - - if (red.length === 0) { - // Finally reached only leafs! - return Promise.all(green.map((p) => this._activateExtension(p.id, p.reason))).then(_ => undefined); - } - - return this._activateExtensions(green).then(_ => { - return this._activateExtensions(red); - }); + await this._activate(); } - private _activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { - const extensionKey = ExtensionIdentifier.toKey(extensionId); - - if (this._activatedExtensions.has(extensionKey)) { - return Promise.resolve(undefined); - } - - const currentlyActivatingExtension = this._activatingExtensions.get(extensionKey); - if (currentlyActivatingExtension) { - return currentlyActivatingExtension; - } - - const newlyActivatingExtension = this._host.actualActivateExtension(extensionId, reason).then(undefined, (err) => { + private async _activate(): Promise { + try { + this._value = await this._host.actualActivateExtension(this._id, this._reason); + } catch (err) { const error = new Error(); if (err && err.name) { error.name = err.name; } if (err && err.message) { - error.message = `Activating extension '${extensionId.value}' failed: ${err.message}.`; + error.message = `Activating extension '${this._id.value}' failed: ${err.message}.`; } else { - error.message = `Activating extension '${extensionId.value}' failed: ${err}.`; + error.message = `Activating extension '${this._id.value}' failed: ${err}.`; } if (err && err.stack) { error.stack = err.stack; } + // Treat the extension as being empty + this._value = new FailedExtension(error); + if (this._isDisposed && errors.isCancellationError(err)) { // It is expected for ongoing activations to fail if the extension host is going down // So simply ignore and don't log canceled errors in this case - return new FailedExtension(err); + return; } - this._host.onExtensionActivationError( - extensionId, - error, - null - ); - this._logService.error(`Activating extension ${extensionId.value} failed due to an error:`); + this._host.onExtensionActivationError(this._id, error, null); + this._logService.error(`Activating extension ${this._id.value} failed due to an error:`); this._logService.error(err); - // Treat the extension as being empty - return new FailedExtension(err); - }).then((x: ActivatedExtension) => { - this._activatedExtensions.set(extensionKey, x); - this._activatingExtensions.delete(extensionKey); - }); - - this._activatingExtensions.set(extensionKey, newlyActivatingExtension); - return newlyActivatingExtension; + } } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 7d9e044afcc..3042ed465c3 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -753,12 +753,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } } - public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { + public async $getCanonicalURI(remoteAuthority: string, uriComponents: UriComponents): Promise { this._logService.info(`$getCanonicalURI invoked for authority (${getRemoteAuthorityPrefix(remoteAuthority)})`); - const { authorityPrefix, resolver } = await this._activateAndGetResolver(remoteAuthority); + const { resolver } = await this._activateAndGetResolver(remoteAuthority); if (!resolver) { - throw new Error(`Cannot get canonical URI because no remote extension is installed to resolve ${authorityPrefix}`); + // Return `null` if no resolver for `remoteAuthority` is found. + return null; } const uri = URI.revive(uriComponents); diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index ed5e1cd908a..3a8829425de 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -178,6 +178,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ this._onDidDeleteFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) })); break; case FileOperation.CREATE: + case FileOperation.COPY: this._onDidCreateFile.fire(Object.freeze({ files: files.map(f => URI.revive(f.target)) })); break; default: @@ -213,6 +214,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ case FileOperation.DELETE: return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); case FileOperation.CREATE: + case FileOperation.COPY: return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token); } return undefined; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 035389dcda5..ab6dbb99bac 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { mixin } from 'vs/base/common/objects'; import type * as vscode from 'vscode'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location } from 'vs/workbench/api/common/extHostTypes'; +import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKindNew } from 'vs/workbench/api/common/extHostTypes'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import * as languages from 'vs/editor/common/languages'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -34,6 +34,7 @@ import { StopWatch } from 'vs/base/common/stopwatch'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { raceCancellationError } from 'vs/base/common/async'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; // --- adapter @@ -1083,7 +1084,7 @@ class InlineCompletionAdapter { throw new Error('text or insertText must be defined'); } return ({ - text: insertText, + insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, range: item.range ? typeConvert.Range.from(item.range) : undefined, command, idx: idx, @@ -1112,6 +1113,99 @@ class InlineCompletionAdapter { } } +class InlineCompletionAdapterNew { + private readonly _cache = new Cache('InlineCompletionItemNew'); + private readonly _disposables = new Map(); + + private readonly isAdditionProposedApiEnabled = isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions'); + + constructor( + private readonly extension: IExtensionDescription, + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineCompletionItemProviderNew, + private readonly _commands: CommandsConverter, + ) { } + + private readonly languageTriggerKindToVSCodeTriggerKind: Record = { + [languages.InlineCompletionTriggerKind.Automatic]: InlineCompletionTriggerKindNew.Automatic, + [languages.InlineCompletionTriggerKind.Explicit]: InlineCompletionTriggerKindNew.Invoke, + }; + + public async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + const result = await this._provider.provideInlineCompletionItems(doc, pos, { + selectedCompletionInfo: + context.selectedSuggestionInfo + ? { + range: typeConvert.Range.to(context.selectedSuggestionInfo.range), + text: context.selectedSuggestionInfo.text + } + : undefined, + triggerKind: this.languageTriggerKindToVSCodeTriggerKind[context.triggerKind] + }, token); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + const normalizedResult = isArray(result) ? result : result.items; + + const pid = this._cache.add(normalizedResult); + let disposableStore: DisposableStore | undefined = undefined; + + return { + pid, + items: normalizedResult.map((item, idx) => { + let command: languages.Command | undefined = undefined; + if (item.command) { + if (!disposableStore) { + disposableStore = new DisposableStore(); + this._disposables.set(pid, disposableStore); + } + command = this._commands.toInternal(item.command, disposableStore); + } + + const insertText = item.insertText; + return ({ + insertText: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, + filterText: item.filterText, + range: item.range ? typeConvert.Range.from(item.range) : undefined, + command, + idx: idx, + completeBracketPairs: this.isAdditionProposedApiEnabled ? item.completeBracketPairs : false + }); + }), + }; + } + + public disposeCompletions(pid: number) { + this._cache.delete(pid); + const d = this._disposables.get(pid); + if (d) { + d.clear(); + } + this._disposables.delete(pid); + } + + public handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._cache.get(pid, idx); + if (completionItem) { + if (this._provider.handleDidShowCompletionItem && isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions')) { + this._provider.handleDidShowCompletionItem(completionItem); + } + } + } +} + export class InlineCompletionController implements vscode.InlineCompletionController { private static readonly map = new WeakMap, InlineCompletionController>(); @@ -1263,6 +1357,7 @@ class InlayHintsAdapter { cacheId: id, tooltip: typeConvert.MarkdownString.fromStrict(hint.tooltip), position: typeConvert.Position.from(hint.position), + textEdits: hint.textEdits && hint.textEdits.map(typeConvert.TextEdit.from), kind: hint.kind && typeConvert.InlayHintKind.from(hint.kind), paddingLeft: hint.paddingLeft, paddingRight: hint.paddingRight, @@ -1615,7 +1710,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter - | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter; + | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter | InlineCompletionAdapterNew; class AdapterData { constructor( @@ -2049,6 +2144,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } + registerInlineCompletionsProviderNew(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineCompletionItemProviderNew): vscode.Disposable { + const handle = this._addNewAdapter(new InlineCompletionAdapterNew(extension, this._documents, provider, this._commands.converter), extension); + this._proxy.$registerInlineCompletionsSupport(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { return this._withAdapter(handle, InlineCompletionAdapter, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token); } diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index 055a0b7424b..58197d4c2c0 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -10,12 +10,25 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { NotebookRange } from 'vs/workbench/api/common/extHostTypes'; import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; class RawContentChangeEvent { - constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: vscode.NotebookCell[], readonly items: ExtHostCell[]) { } + + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: vscode.NotebookCell[], readonly items: ExtHostCell[]) { + + } + + asApiEvent(): vscode.NotebookDocumentContentChange { + return { + range: new NotebookRange(this.start, this.start + this.deletedCount), + addedCells: this.items.map(cell => cell.apiCell), + removedCells: this.deletedItems, + }; + } + static asApiEvents(events: RawContentChangeEvent[]): readonly vscode.NotebookCellsChangeData[] { return events.map(event => { @@ -80,7 +93,7 @@ export class ExtHostCell { if (!data) { throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`); } - this._apiCell = Object.freeze({ + const apiCell: vscode.NotebookCell = { get index() { return that.notebook.getCellIndex(that); }, notebook: that.notebook.apiNotebook, kind: extHostTypeConverters.NotebookCellKind.to(this._cellData.cellKind), @@ -90,7 +103,8 @@ export class ExtHostCell { get outputs() { return that._outputs.slice(0); }, get metadata() { return that._metadata; }, get executionSummary() { return that._previousResult; } - }); + }; + this._apiCell = Object.freeze(apiCell); } return this._apiCell; } @@ -157,7 +171,7 @@ export class ExtHostNotebookDocument { ) { this._notebookType = data.viewType; this._metadata = Object.freeze(data.metadata ?? Object.create(null)); - this._spliceNotebookCells([[0, 0, data.cells]], true /* init -> no event*/); + this._spliceNotebookCells([[0, 0, data.cells]], true /* init -> no event*/, undefined); this._versionId = data.versionId; } @@ -168,7 +182,7 @@ export class ExtHostNotebookDocument { get apiNotebook(): vscode.NotebookDocument { if (!this._notebook) { const that = this; - this._notebook = { + const apiObject: vscode.NotebookDocument = { get uri() { return that.uri; }, get version() { return that._versionId; }, get notebookType() { return that._notebookType; }, @@ -189,6 +203,7 @@ export class ExtHostNotebookDocument { return that._save(); } }; + this._notebook = Object.freeze(apiObject); } return this._notebook; } @@ -213,29 +228,80 @@ export class ExtHostNotebookDocument { this._isDirty = isDirty; } - acceptModelChanged(event: extHostProtocol.NotebookCellsChangedEventDto, isDirty: boolean): void { + acceptModelChanged(event: extHostProtocol.NotebookCellsChangedEventDto, isDirty: boolean, newMetadata: notebookCommon.NotebookDocumentMetadata | undefined): vscode.NotebookDocumentChangeEvent { this._versionId = event.versionId; this._isDirty = isDirty; + this.acceptDocumentPropertiesChanged({ metadata: newMetadata }); + + const result = { + notebook: this.apiNotebook, + metadata: newMetadata, + cellChanges: [], + contentChanges: [], + }; + + type RelaxedCellChange = Partial & { cell: vscode.NotebookCell }; + const relaxedCellChanges: RelaxedCellChange[] = []; + + // -- apply change and populate content changes for (const rawEvent of event.rawEvents) { if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(rawEvent.changes, false); + this._spliceNotebookCells(rawEvent.changes, false, result.contentChanges); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.Move) { - this._moveCell(rawEvent.index, rawEvent.newIdx); + this._moveCell(rawEvent.index, rawEvent.newIdx, result.contentChanges); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.Output) { this._setCellOutputs(rawEvent.index, rawEvent.outputs); + relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, outputs: this._cells[rawEvent.index].apiCell.outputs }); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.OutputItem) { this._setCellOutputItems(rawEvent.index, rawEvent.outputId, rawEvent.append, rawEvent.outputItems); + relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, outputs: this._cells[rawEvent.index].apiCell.outputs }); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeLanguage) { this._changeCellLanguage(rawEvent.index, rawEvent.language); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMime) { this._changeCellMime(rawEvent.index, rawEvent.mime); } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellMetadata) { this._changeCellMetadata(rawEvent.index, rawEvent.metadata); + relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, metadata: this._cells[rawEvent.index].apiCell.metadata }); + } else if (rawEvent.kind === notebookCommon.NotebookCellsChangeType.ChangeCellInternalMetadata) { this._changeCellInternalMetadata(rawEvent.index, rawEvent.internalMetadata); + relaxedCellChanges.push({ cell: this._cells[rawEvent.index].apiCell, executionSummary: this._cells[rawEvent.index].apiCell.executionSummary }); } } + + // -- compact cellChanges + + const map = new Map(); + for (let i = 0; i < relaxedCellChanges.length; i++) { + const relaxedCellChange = relaxedCellChanges[i]; + const existing = map.get(relaxedCellChange.cell); + if (existing === undefined) { + const newLen = result.cellChanges.push({ + executionSummary: undefined, + metadata: undefined, + outputs: undefined, + ...relaxedCellChange, + }); + map.set(relaxedCellChange.cell, newLen - 1); + } else { + result.cellChanges[existing] = { + ...result.cellChanges[existing], + ...relaxedCellChange + }; + } + } + + // Freeze event properties so handlers cannot accidentally modify them + Object.freeze(result); + Object.freeze(result.cellChanges); + Object.freeze(result.contentChanges); + + return result; } private _validateIndex(index: number): number { @@ -277,7 +343,7 @@ export class ExtHostNotebookDocument { return this._proxy.$trySaveNotebook(this.uri); } - private _spliceNotebookCells(splices: notebookCommon.NotebookCellTextModelSplice[], initialization: boolean): void { + private _spliceNotebookCells(splices: notebookCommon.NotebookCellTextModelSplice[], initialization: boolean, bucket: vscode.NotebookDocumentContentChange[] | undefined): void { if (this._disposed) { return; } @@ -303,7 +369,6 @@ export class ExtHostNotebookDocument { removedCellDocuments.push(cell.uri); changeEvent.deletedItems.push(cell.apiCell); } - contentChangeEvents.push(changeEvent); }); @@ -312,6 +377,12 @@ export class ExtHostNotebookDocument { removedDocuments: removedCellDocuments }); + if (bucket) { + for (let changeEvent of contentChangeEvents) { + bucket.push(changeEvent.asApiEvent()); + } + } + if (!initialization) { this._emitter.emitModelChange(deepFreeze({ document: this.apiNotebook, @@ -320,13 +391,16 @@ export class ExtHostNotebookDocument { } } - private _moveCell(index: number, newIdx: number): void { + private _moveCell(index: number, newIdx: number, bucket: vscode.NotebookDocumentContentChange[]): void { const cells = this._cells.splice(index, 1); this._cells.splice(newIdx, 0, ...cells); const changes = [ new RawContentChangeEvent(index, 1, cells.map(c => c.apiCell), []), new RawContentChangeEvent(newIdx, 0, [], cells) ]; + for (const change of changes) { + bucket.push(change.asApiEvent()); + } this._emitter.emitModelChange(deepFreeze({ document: this.apiNotebook, changes: RawContentChangeEvent.asApiEvents(changes) diff --git a/src/vs/workbench/api/common/extHostNotebookDocuments.ts b/src/vs/workbench/api/common/extHostNotebookDocuments.ts index 70db04b1181..fbb400ec64d 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocuments.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocuments.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; +import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; @@ -16,17 +17,21 @@ export class ExtHostNotebookDocuments implements extHostProtocol.ExtHostNotebook private readonly _onDidChangeNotebookDocumentMetadata = new Emitter(); readonly onDidChangeNotebookDocumentMetadata = this._onDidChangeNotebookDocumentMetadata.event; - private _onDidSaveNotebookDocument = new Emitter(); + private readonly _onDidSaveNotebookDocument = new Emitter(); readonly onDidSaveNotebookDocument = this._onDidSaveNotebookDocument.event; + private readonly _onDidChangeNotebookDocument = new Emitter(); + readonly onDidChangeNotebookDocument = this._onDidChangeNotebookDocument.event; + constructor( @ILogService private readonly _logService: ILogService, private readonly _notebooksAndEditors: ExtHostNotebookController, ) { } - $acceptModelChanged(uri: UriComponents, event: SerializableObjectWithBuffers, isDirty: boolean): void { + $acceptModelChanged(uri: UriComponents, event: SerializableObjectWithBuffers, isDirty: boolean, newMetadata?: NotebookDocumentMetadata): void { const document = this._notebooksAndEditors.getNotebookDocument(URI.revive(uri)); - document.acceptModelChanged(event.value, isDirty); + const e = document.acceptModelChanged(event.value, isDirty, newMetadata); + this._onDidChangeNotebookDocument.fire(e); } $acceptDirtyStateChanged(uri: UriComponents, isDirty: boolean): void { diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index a2f6efd3cbc..0df03b79137 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -9,7 +9,7 @@ import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; -import type { InputBox, InputBoxOptions, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import type { InputBox, InputBoxOptions, InputBoxValidationSeverity, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, TransferQuickInputButton, TransferQuickPickItemOrSeparator } from './extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { ThemeIcon, QuickInputButtons, QuickPickItemKind } from 'vs/workbench/api/common/extHostTypes'; @@ -18,6 +18,7 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio import { coalesce } from 'vs/base/common/arrays'; import Severity from 'vs/base/common/severity'; import { ThemeIcon as ThemeIconUtils } from 'vs/platform/theme/common/themeService'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; export type Item = string | QuickPickItem; @@ -31,9 +32,9 @@ export interface ExtHostQuickOpen { showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions, token?: CancellationToken): Promise; - createQuickPick(extensionId: IExtensionDescription): QuickPick; + createQuickPick(extension: IExtensionDescription): QuickPick; - createInputBox(extensionId: ExtensionIdentifier): InputBox; + createInputBox(extension: IExtensionDescription): InputBox; } export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IExtHostWorkspaceProvider, commands: ExtHostCommands): ExtHostQuickOpenShape & ExtHostQuickOpen { @@ -45,7 +46,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _commands: ExtHostCommands; private _onDidSelectItem?: (handle: number) => void; - private _validateInput?: (input: string) => string | undefined | null | Thenable; + private _validateInput?: (input: string) => string | { content: string; severity: Severity } | undefined | null | Thenable; private _sessions = new Map(); @@ -159,7 +160,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx }); } - $validateInput(input: string): Promise { + $validateInput(input: string): Promise { if (this._validateInput) { return asPromise(() => this._validateInput!(input)); } @@ -188,8 +189,8 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return session; } - createInputBox(extensionId: ExtensionIdentifier): InputBox { - const session: ExtHostInputBox = new ExtHostInputBox(extensionId, () => this._sessions.delete(session._id)); + createInputBox(extension: IExtensionDescription): InputBox { + const session: ExtHostInputBox = new ExtHostInputBox(extension, () => this._sessions.delete(session._id)); this._sessions.set(session._id, session); return session; } @@ -674,9 +675,10 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _password = false; private _prompt: string | undefined; private _validationMessage: string | undefined; + private _validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; - constructor(extensionId: ExtensionIdentifier, onDispose: () => void) { - super(extensionId, onDispose); + constructor(private readonly extension: IExtensionDescription, onDispose: () => void) { + super(extension.identifier, onDispose); this.update({ type: 'inputBox' }); } @@ -706,6 +708,22 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx this._validationMessage = validationMessage; this.update({ validationMessage, severity: validationMessage ? Severity.Error : Severity.Ignore }); } + + get validationMessage2() { + return this._validationMessage2; + } + + set validationMessage2(validationMessage: string | { content: string; severity: InputBoxValidationSeverity } | undefined) { + checkProposedApiEnabled(this.extension, 'inputBoxSeverity'); + this._validationMessage2 = validationMessage; + if (!validationMessage) { + this.update({ validationMessage: undefined, severity: Severity.Ignore }); + } else if (typeof validationMessage === 'string') { + this.update({ validationMessage, severity: Severity.Error }); + } else { + this.update({ validationMessage: validationMessage.content, severity: validationMessage.severity ?? Severity.Error }); + } + } } return new ExtHostQuickOpenImpl(workspace, commands); diff --git a/src/vs/workbench/api/common/extHostTelemetry.ts b/src/vs/workbench/api/common/extHostTelemetry.ts index 5136b494011..d27d0a9047d 100644 --- a/src/vs/workbench/api/common/extHostTelemetry.ts +++ b/src/vs/workbench/api/common/extHostTelemetry.ts @@ -16,6 +16,7 @@ export class ExtHostTelemetry implements ExtHostTelemetryShape { private readonly _onDidChangeTelemetryConfiguration = new Emitter(); readonly onDidChangeTelemetryConfiguration: Event = this._onDidChangeTelemetryConfiguration.event; + private _productConfig: { usage: boolean; error: boolean } = { usage: true, error: true }; private _level: TelemetryLevel = TelemetryLevel.NONE; private _oldTelemetryEnablement: boolean | undefined; @@ -26,13 +27,14 @@ export class ExtHostTelemetry implements ExtHostTelemetryShape { getTelemetryDetails(): TelemetryConfiguration { return { isCrashEnabled: this._level >= TelemetryLevel.CRASH, - isErrorsEnabled: this._level >= TelemetryLevel.ERROR, - isUsageEnabled: this._level >= TelemetryLevel.USAGE + isErrorsEnabled: this._productConfig.error ? this._level >= TelemetryLevel.ERROR : false, + isUsageEnabled: this._productConfig.usage ? this._level >= TelemetryLevel.USAGE : false }; } - $initializeTelemetryLevel(level: TelemetryLevel): void { + $initializeTelemetryLevel(level: TelemetryLevel, productConfig?: { usage: boolean; error: boolean }): void { this._level = level; + this._productConfig = productConfig || { usage: true, error: true }; } $onDidChangeTelemetryLevel(level: TelemetryLevel): void { diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 59a1af06494..70ea3824623 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -95,9 +95,7 @@ export class ExtHostTesting implements ExtHostTestingShape { profileId++; } - const profile = new TestRunProfileImpl(this.proxy, controllerId, profileId, label, group, runHandler, isDefault, tag); - profiles.set(profileId, profile); - return profile; + return new TestRunProfileImpl(this.proxy, profiles, controllerId, profileId, label, group, runHandler, isDefault, tag); }, createTestItem(id, label, uri) { return new TestItemImpl(controllerId, id, label, uri); @@ -153,7 +151,7 @@ export class ExtHostTesting implements ExtHostTestingShape { await this.proxy.$runTests({ isUiTriggered: false, targets: [{ - testIds: req.include?.map(t => t.id) ?? [controller.collection.root.id], + testIds: req.include?.map(t => TestId.fromExtHostTestItem(t, controller.collection.root.id).toString()) ?? [controller.collection.root.id], profileGroup: profileGroupToBitset[profile.kind], profileId: profile.profileId, controllerId: profile.controllerId, @@ -542,9 +540,9 @@ export class TestRunCoordinator { this.proxy.$startedExtensionTestRun({ controllerId, profile: profile && { group: profileGroupToBitset[profile.kind], id: profile.profileId }, - exclude: request.exclude?.map(t => t.id) ?? [], + exclude: request.exclude?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [], id: dto.id, - include: request.include?.map(t => t.id) ?? [collection.root.id], + include: request.include?.map(t => TestId.fromExtHostTestItem(t, collection.root.id).toString()) ?? [collection.root.id], persist }); @@ -875,6 +873,7 @@ class TestObservers { export class TestRunProfileImpl implements vscode.TestRunProfile { readonly #proxy: MainThreadTestingShape; + #profiles?: Map; private _configureHandler?: (() => void); public get label() { @@ -925,6 +924,7 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { constructor( proxy: MainThreadTestingShape, + profiles: Map, public readonly controllerId: string, public readonly profileId: number, private _label: string, @@ -934,6 +934,8 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { public _tag: vscode.TestTag | undefined = undefined, ) { this.#proxy = proxy; + this.#profiles = profiles; + profiles.set(profileId, this); const groupBitset = profileGroupToBitset[kind]; if (typeof groupBitset !== 'number') { @@ -952,7 +954,10 @@ export class TestRunProfileImpl implements vscode.TestRunProfile { } dispose(): void { - this.#proxy.$removeTestProfile(this.controllerId, this.profileId); + if (this.#profiles?.delete(this.profileId)) { + this.#profiles = undefined; + this.#proxy.$removeTestProfile(this.controllerId, this.profileId); + } } } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index fac8326994b..db69a6a67bc 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -3,15 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from 'vs/base/common/event'; +import { AsyncEmitter, Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; -import type * as vscode from 'vscode'; +import * as vscode from 'vscode'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; +import { IPosition } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -21,6 +24,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { private readonly _onDidChangeTextEditorViewColumn = new Emitter(); private readonly _onDidChangeActiveTextEditor = new Emitter(); private readonly _onDidChangeVisibleTextEditors = new Emitter(); + private readonly _onWillDropOnTextEditor = new AsyncEmitter(); readonly onDidChangeTextEditorSelection: Event = this._onDidChangeTextEditorSelection.event; readonly onDidChangeTextEditorOptions: Event = this._onDidChangeTextEditorOptions.event; @@ -28,6 +32,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeTextEditorViewColumn: Event = this._onDidChangeTextEditorViewColumn.event; readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; + readonly onWillDropOnTextEditor: Event = this._onWillDropOnTextEditor.event; private readonly _proxy: MainThreadTextEditorsShape; @@ -159,4 +164,24 @@ export class ExtHostEditors implements ExtHostEditorsShape { getDiffInformation(id: string): Promise { return Promise.resolve(this._proxy.$getDiffInformation(id)); } + + // --- Text editor drag and drop + + async $textEditorHandleDrop(id: string, position: IPosition, dataTransferDto: DataTransferDTO): Promise { + const textEditor = this._extHostDocumentsAndEditors.getEditor(id); + if (!textEditor) { + throw new Error('Unknown text editor'); + } + + const pos = TypeConverters.Position.to(position); + const dataTransfer = DataTransferConverter.toDataTransfer(dataTransferDto); + + const event = Object.freeze({ + editor: textEditor.value, + position: pos, + dataTransfer: dataTransfer + }); + + await this._onWillDropOnTextEditor.fireAsync(event, CancellationToken.None); + } } diff --git a/src/vs/workbench/api/common/extHostTheming.ts b/src/vs/workbench/api/common/extHostTheming.ts index 04c3d517fdc..1bcb3cad0b7 100644 --- a/src/vs/workbench/api/common/extHostTheming.ts +++ b/src/vs/workbench/api/common/extHostTheming.ts @@ -27,7 +27,14 @@ export class ExtHostTheming implements ExtHostThemingShape { } $onColorThemeChange(type: string): void { - let kind = type === 'light' ? ColorThemeKind.Light : type === 'dark' ? ColorThemeKind.Dark : ColorThemeKind.HighContrast; + let kind; + switch (type) { + case 'light': kind = ColorThemeKind.Light; break; + case 'hcDark': kind = ColorThemeKind.HighContrast; break; + case 'hcLight': kind = ColorThemeKind.HighContrastLight; break; + default: + kind = ColorThemeKind.Dark; + } this._actual = new ColorTheme(kind); this._onDidChangeActiveColorTheme.fire(this._actual); } diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 1aae2e74c6b..baab688ad31 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -145,12 +145,21 @@ export class ExtHostTimeline implements IExtHostTimeline { } } - let detail; - if (MarkdownStringType.isMarkdownString(props.detail)) { - detail = MarkdownString.from(props.detail); + let tooltip; + if (MarkdownStringType.isMarkdownString(props.tooltip)) { + tooltip = MarkdownString.from(props.tooltip); } - else if (isString(props.detail)) { - detail = props.detail; + else if (isString(props.tooltip)) { + tooltip = props.tooltip; + } + // TODO @jkearl, remove once migration complete. + else if (MarkdownStringType.isMarkdownString((props as any).detail)) { + console.warn('Using deprecated TimelineItem.detail, migrate to TimelineItem.tooltip'); + tooltip = MarkdownString.from((props as any).detail); + } + else if (isString((props as any).detail)) { + console.warn('Using deprecated TimelineItem.detail, migrate to TimelineItem.tooltip'); + tooltip = (props as any).detail; } return { @@ -162,7 +171,7 @@ export class ExtHostTimeline implements IExtHostTimeline { icon: icon, iconDark: iconDark, themeIcon: themeIcon, - detail, + tooltip, accessibilityInformation: item.accessibilityInformation }; }; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 560af394957..29524c764e9 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; -import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; import { TreeItemCollapsibleState, ThemeIcon, MarkdownString as MarkdownStringType } from 'vs/workbench/api/common/extHostTypes'; @@ -22,8 +22,9 @@ import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/languages'; -import { TreeDataTransferConverter, TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; +import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; type TreeItemHandle = string; @@ -50,7 +51,7 @@ function toTreeItemLabel(label: any, extension: IExtensionDescription): ITreeIte export class ExtHostTreeViews implements ExtHostTreeViewsShape { private treeViews: Map> = new Map>(); - private treeDragAndDropService: ITreeViewsService = new TreeviewsService(); + private treeDragAndDropService: ITreeViewsService = new TreeviewsService(); constructor( private _proxy: MainThreadTreeViewsShape, @@ -134,22 +135,22 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } - async $handleDrop(destinationViewId: string, treeDataTransferDTO: TreeDataTransferDTO, newParentItemHandle: string, token: CancellationToken, + async $handleDrop(destinationViewId: string, treeDataTransferDTO: DataTransferDTO, newParentItemHandle: string, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise { const treeView = this.treeViews.get(destinationViewId); if (!treeView) { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', destinationViewId))); } - const treeDataTransfer = TreeDataTransferConverter.toITreeDataTransfer(treeDataTransferDTO); + const treeDataTransfer = DataTransferConverter.toDataTransfer(treeDataTransferDTO); if ((sourceViewId === destinationViewId) && sourceTreeItemHandles) { await this.addAdditionalTransferItems(treeDataTransfer, treeView, sourceTreeItemHandles, token, operationUuid); } return treeView.onDrop(treeDataTransfer, newParentItemHandle, token); } - private async addAdditionalTransferItems(treeDataTransfer: ITreeDataTransfer, treeView: ExtHostTreeView, - sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise { + private async addAdditionalTransferItems(treeDataTransfer: IDataTransfer, treeView: ExtHostTreeView, + sourceTreeItemHandles: string[], token: CancellationToken, operationUuid?: string): Promise { const existingTransferOperation = this.treeDragAndDropService.removeDragOperationTransfer(operationUuid); if (existingTransferOperation) { (await existingTransferOperation)?.forEach((value, key) => { @@ -165,7 +166,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeDataTransfer; } - async $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { + async $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise { const treeView = this.treeViews.get(sourceViewId); if (!treeView) { return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', sourceViewId))); @@ -176,7 +177,7 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return; } - return TreeDataTransferConverter.toTreeDataTransferDTO(treeDataTransfer); + return DataTransferConverter.toDataTransferDTO(treeDataTransfer); } async $hasResolve(treeViewId: string): Promise { @@ -296,10 +297,8 @@ class ExtHostTreeView extends Disposable { } this.dataProvider = options.treeDataProvider; this.dndController = options.dragAndDropController; - if (this.dataProvider.onDidChangeTreeData2) { - this._register(this.dataProvider.onDidChangeTreeData2(elementOrElements => this._onDidChangeData.fire({ message: false, element: elementOrElements }))); - } else if (this.dataProvider.onDidChangeTreeData) { - this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element }))); + if (this.dataProvider.onDidChangeTreeData) { + this._register(this.dataProvider.onDidChangeTreeData(elementOrElements => this._onDidChangeData.fire({ message: false, element: elementOrElements }))); } let refreshingPromise: Promise | null; @@ -433,7 +432,7 @@ class ExtHostTreeView extends Disposable { } } - async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: ITreeDataTransfer, token: CancellationToken): Promise { + async handleDrag(sourceTreeItemHandles: TreeItemHandle[], treeDataTransfer: IDataTransfer, token: CancellationToken): Promise { const extensionTreeItems: T[] = []; for (const sourceHandle of sourceTreeItemHandles) { const extensionItem = this.getExtensionElement(sourceHandle); @@ -453,7 +452,7 @@ class ExtHostTreeView extends Disposable { return !!this.dndController?.handleDrag; } - async onDrop(treeDataTransfer: vscode.TreeDataTransfer, targetHandleOrNode: TreeItemHandle, token: CancellationToken): Promise { + async onDrop(treeDataTransfer: vscode.DataTransfer, targetHandleOrNode: TreeItemHandle, token: CancellationToken): Promise { const target = this.getExtensionElement(targetHandleOrNode); if (!target || !this.dndController?.handleDrop) { return; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 396202a6722..28586947531 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1170,6 +1170,7 @@ export namespace InlayHint { typeof hint.label === 'string' ? hint.label : hint.label.map(InlayHintLabelPart.to.bind(undefined, converter)), hint.kind && InlayHintKind.to(hint.kind) ); + res.textEdits = hint.textEdits && hint.textEdits.map(TextEdit.to); res.tooltip = htmlContent.isMarkdownString(hint.tooltip) ? MarkdownString.to(hint.tooltip) : hint.tooltip; res.paddingLeft = hint.paddingLeft; res.paddingRight = hint.paddingRight; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index cf9c4fd35a7..584f7dfc14e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1467,6 +1467,7 @@ export class InlayHint implements vscode.InlayHint { label: string | InlayHintLabelPart[]; tooltip?: string | vscode.MarkdownString; position: Position; + textEdits?: TextEdit[]; kind?: vscode.InlayHintKind; paddingLeft?: boolean; paddingRight?: boolean; @@ -1611,6 +1612,28 @@ export class InlineSuggestions implements vscode.InlineCompletionList { } } +@es5ClassCompat +export class InlineSuggestionNew implements vscode.InlineCompletionItemNew { + insertText: string; + range?: Range; + command?: vscode.Command; + + constructor(insertText: string, range?: Range, command?: vscode.Command) { + this.insertText = insertText; + this.range = range; + this.command = command; + } +} + +@es5ClassCompat +export class InlineSuggestionsNew implements vscode.InlineCompletionListNew { + items: vscode.InlineCompletionItemNew[]; + + constructor(items: vscode.InlineCompletionItemNew[]) { + this.items = items; + } +} + export enum ViewColumn { Active = -1, Beside = -2, @@ -2348,7 +2371,7 @@ export enum TreeItemCollapsibleState { } @es5ClassCompat -export class TreeDataTransferItem { +export class DataTransferItem { async asString(): Promise { return typeof this.value === 'string' ? this.value : JSON.stringify(this.value); } @@ -2357,7 +2380,7 @@ export class TreeDataTransferItem { } @es5ClassCompat -export class TreeDataTransfer { +export class DataTransfer { private readonly _items: Map = new Map(); get(mimeType: string): T | undefined { return this._items.get(mimeType); @@ -2586,6 +2609,11 @@ export enum InlineCompletionTriggerKind { Explicit = 1, } +export enum InlineCompletionTriggerKindNew { + Invoke = 0, + Automatic = 1, +} + @es5ClassCompat export class InlineValueText implements vscode.InlineValueText { readonly range: Range; @@ -2994,6 +3022,12 @@ export enum QuickPickItemKind { Default = 0, } +export enum InputBoxValidationSeverity { + Info = 1, + Warning = 2, + Error = 3 +} + export enum ExtensionKind { UI = 1, Workspace = 2 @@ -3040,7 +3074,8 @@ export class ColorTheme implements vscode.ColorTheme { export enum ColorThemeKind { Light = 1, Dark = 2, - HighContrast = 3 + HighContrast = 3, + HighContrastLight = 4 } //#endregion Theming @@ -3203,7 +3238,7 @@ export class NotebookCellOutputItem { return new NotebookCellOutputItem(bytes, mime); } - static json(value: any, mime: string = 'application/json'): NotebookCellOutputItem { + static json(value: any, mime: string = 'text/x-json'): NotebookCellOutputItem { const rawStr = JSON.stringify(value, undefined, '\t'); return NotebookCellOutputItem.text(rawStr, mime); } @@ -3566,3 +3601,26 @@ export class TypeHierarchyItem { this.selectionRange = selectionRange; } } + +//#region Tab Inputs + +export class TextTabInput { + constructor(readonly uri: URI) { } +} + +export class TextDiffTabInput { + constructor(readonly original: URI, readonly modified: URI) { } +} + +export class CustomEditorTabInput { + constructor(readonly uri: URI, readonly viewType: string) { } +} + +export class NotebookEditorTabInput { + constructor(readonly uri: URI, readonly notebookType: string) { } +} + +export class NotebookDiffEditorTabInput { + constructor(readonly original: URI, readonly modified: URI, readonly notebookType: string) { } +} +//#endregion diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index d0abc76fdd1..db22cb7fc13 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { serializeWebviewMessage, deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { asWebviewUri, webviewGenericCspSource, WebviewInitData } from 'vs/workbench/common/webview'; +import { asWebviewUri, webviewGenericCspSource, WebviewRemoteInfo } from 'vs/workbench/common/webview'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import type * as vscode from 'vscode'; import * as extHostProtocol from './extHost.protocol'; @@ -24,7 +24,7 @@ export class ExtHostWebview implements vscode.Webview { readonly #proxy: extHostProtocol.MainThreadWebviewsShape; readonly #deprecationService: IExtHostApiDeprecationService; - readonly #initData: WebviewInitData; + readonly #remoteInfo: WebviewRemoteInfo; readonly #workspace: IExtHostWorkspace | undefined; readonly #extension: IExtensionDescription; @@ -39,7 +39,7 @@ export class ExtHostWebview implements vscode.Webview { handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewsShape, options: vscode.WebviewOptions, - initData: WebviewInitData, + remoteInfo: WebviewRemoteInfo, workspace: IExtHostWorkspace | undefined, extension: IExtensionDescription, deprecationService: IExtHostApiDeprecationService, @@ -47,7 +47,7 @@ export class ExtHostWebview implements vscode.Webview { this.#handle = handle; this.#proxy = proxy; this.#options = options; - this.#initData = initData; + this.#remoteInfo = remoteInfo; this.#workspace = workspace; this.#extension = extension; this.#serializeBuffersForPostMessage = shouldSerializeBuffersForPostMessage(extension); @@ -71,7 +71,7 @@ export class ExtHostWebview implements vscode.Webview { public asWebviewUri(resource: vscode.Uri): vscode.Uri { this.#hasCalledAsWebviewUri = true; - return asWebviewUri(resource, this.#initData.remote); + return asWebviewUri(resource, this.#remoteInfo); } public get cspSource(): string { @@ -150,7 +150,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { constructor( mainContext: extHostProtocol.IMainContext, - private readonly initData: WebviewInitData, + private readonly remoteInfo: WebviewRemoteInfo, private readonly workspace: IExtHostWorkspace | undefined, private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, @@ -178,7 +178,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { } public createNewWebview(handle: string, options: extHostProtocol.IWebviewContentOptions, extension: IExtensionDescription): ExtHostWebview { - const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); + const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.remoteInfo, this.workspace, extension, this._deprecationService); this._webviews.set(handle, webview); webview._onDidDispose(() => { this._webviews.delete(handle); }); diff --git a/src/vs/workbench/api/common/shared/treeDataTransfer.ts b/src/vs/workbench/api/common/shared/dataTransfer.ts similarity index 59% rename from src/vs/workbench/api/common/shared/treeDataTransfer.ts rename to src/vs/workbench/api/common/shared/dataTransfer.ts index 18936dca2f3..d062e9d2d91 100644 --- a/src/vs/workbench/api/common/shared/treeDataTransfer.ts +++ b/src/vs/workbench/api/common/shared/dataTransfer.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITreeDataTransfer, ITreeDataTransferItem } from 'vs/workbench/common/views'; +import { IDataTransfer, IDataTransferItem } from 'vs/workbench/common/dnd'; -interface TreeDataTransferItemDTO { +interface DataTransferItemDTO { asString: string; } -export interface TreeDataTransferDTO { +export interface DataTransferDTO { types: string[]; - items: TreeDataTransferItemDTO[]; + items: DataTransferItemDTO[]; } -export namespace TreeDataTransferConverter { - export function toITreeDataTransfer(value: TreeDataTransferDTO): ITreeDataTransfer { - const newDataTransfer: ITreeDataTransfer = new Map(); +export namespace DataTransferConverter { + export function toDataTransfer(value: DataTransferDTO): IDataTransfer { + const newDataTransfer: IDataTransfer = new Map(); value.types.forEach((type, index) => { newDataTransfer.set(type, { asString: async () => value.items[index].asString, @@ -26,8 +26,8 @@ export namespace TreeDataTransferConverter { return newDataTransfer; } - export async function toTreeDataTransferDTO(value: ITreeDataTransfer): Promise { - const newDTO: TreeDataTransferDTO = { + export async function toDataTransferDTO(value: IDataTransfer): Promise { + const newDTO: DataTransferDTO = { types: [], items: [] }; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index c43ca682255..9867e2a5e6b 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -38,7 +38,7 @@ export class NativeExtHostSearch extends ExtHostSearch { super(extHostRpc, _uriTransformer, _logService); const outputChannel = new OutputChannel('RipgrepSearchUD', this._logService); - this.registerTextSearchProvider(Schemas.userData, new RipgrepSearchProvider(outputChannel)); + this.registerTextSearchProvider(Schemas.vscodeUserData, new RipgrepSearchProvider(outputChannel)); if (initData.remote.isRemote && initData.remote.authority) { this._registerEHSearchProviders(); } diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index 4535167232f..c22033dd8a5 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -1283,6 +1283,7 @@ suite('ExtHostLanguageFeatureCommands', function () { disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { provideInlayHints() { const hint = new types.InlayHint(new types.Position(0, 1), 'Foo', types.InlayHintKind.Parameter); + hint.textEdits = [types.TextEdit.insert(new types.Position(0, 0), 'Hello')]; return [hint]; } })); @@ -1296,6 +1297,8 @@ suite('ExtHostLanguageFeatureCommands', function () { assert.strictEqual(first.label, 'Foo'); assert.strictEqual(first.position.line, 0); assert.strictEqual(first.position.character, 1); + assert.strictEqual(first.textEdits?.length, 1); + assert.strictEqual(first.textEdits![0].newText, 'Hello'); assert.strictEqual(second.position.line, 10); assert.strictEqual(second.position.character, 11); diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts new file mode 100644 index 00000000000..3391405de52 --- /dev/null +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -0,0 +1,495 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import assert = require('assert'); +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; +import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; +import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; + +suite('ExtHostEditorTabs', function () { + + const defaultTabDto: IEditorTabDto = { + id: 'uniquestring', + input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/def.txt') }, + isActive: true, + isDirty: true, + isPinned: true, + isPreview: false, + label: 'label1', + }; + + function createTabDto(dto?: Partial): IEditorTabDto { + return { ...defaultTabDto, ...dto }; + } + + test('empty', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, undefined); + }); + + test('single tab', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + const [first] = extHostEditorTabs.tabGroups.groups; + assert.ok(first.activeTab); + assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); + + { + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + const [first] = extHostEditorTabs.tabGroups.groups; + assert.ok(first.activeTab); + assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); + } + }); + + test('Empty tab group', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + const [first] = extHostEditorTabs.tabGroups.groups; + assert.strictEqual(first.activeTab, undefined); + assert.strictEqual(first.tabs.length, 0); + }); + + test('Ensure tabGroup change events fires', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + let count = 0; + extHostEditorTabs.tabGroups.onDidChangeTabGroup(() => count++); + + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, undefined); + assert.strictEqual(count, 0); + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }]); + assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); + const activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(activeTabGroup.tabs.length, 0); + assert.strictEqual(count, 1); + }); + + test('Ensure reference equality for activeTab and activeGroup', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + const tab = createTabDto({ + id: 'uniquestring', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + editorId: 'default', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + const [first] = extHostEditorTabs.tabGroups.groups; + assert.ok(first.activeTab); + assert.strictEqual(first.tabs.indexOf(first.activeTab), 0); + assert.strictEqual(first.activeTab, first.tabs[0]); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, first); + }); + + // TODO @lramos15 Change this test because now it only fires when id changes + test.skip('onDidChangeActiveTabGroup fires properly', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + let count = 0; + let activeTabGroupFromEvent: vscode.TabGroup | undefined = undefined; + extHostEditorTabs.tabGroups.onDidChangeActiveTabGroup((tabGroup) => { + count++; + activeTabGroupFromEvent = tabGroup; + }); + + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, undefined); + assert.strictEqual(count, 0); + const tabModel = [{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [], + activeTab: undefined + }]; + extHostEditorTabs.$acceptEditorTabModel(tabModel); + assert.ok(extHostEditorTabs.tabGroups.activeTabGroup); + let activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + assert.strictEqual(count, 1); + assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); + // Firing again with same model shouldn't cause a change + extHostEditorTabs.$acceptEditorTabModel(tabModel); + assert.strictEqual(count, 1); + // Changing a property should fire a change + tabModel[0].viewColumn = 1; + extHostEditorTabs.$acceptEditorTabModel(tabModel); + assert.strictEqual(count, 2); + activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); + // Changing the active tab group should fire a change + tabModel[0].isActive = false; + tabModel.push({ + isActive: true, + viewColumn: 0, + groupId: 13, + tabs: [], + activeTab: undefined + }); + extHostEditorTabs.$acceptEditorTabModel(tabModel); + assert.strictEqual(count, 3); + activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); + + // Empty tab model should fire a change and return undefined + extHostEditorTabs.$acceptEditorTabModel([]); + assert.strictEqual(count, 4); + activeTabGroup = extHostEditorTabs.tabGroups.activeTabGroup; + assert.strictEqual(activeTabGroup, undefined); + assert.strictEqual(activeTabGroup, activeTabGroupFromEvent); + }); + + test('Ensure reference stability', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + const tabDto = createTabDto(); + + // single dirty tab + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tabDto] + }]); + let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + assert.strictEqual(all.length, 1); + const apiTab1 = all[0]; + assert.ok(apiTab1.input instanceof TextTabInput); + assert.strictEqual(tabDto.input.kind, TabInputKind.TextInput); + const dtoResource = (tabDto.input as TextInputDto).uri; + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); + assert.strictEqual(apiTab1.isDirty, true); + + + // NOT DIRTY anymore + + const tabDto2: IEditorTabDto = { ...tabDto, isDirty: false }; + // Accept a simple update + extHostEditorTabs.$acceptTabUpdate(12, tabDto2); + + all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + assert.strictEqual(all.length, 1); + const apiTab2 = all[0]; + assert.ok(apiTab1.input instanceof TextTabInput); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); + assert.strictEqual(apiTab2.isDirty, false); + + assert.strictEqual(apiTab1 === apiTab2, true); + }); + + test('Tab.isActive working', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + const tabDtoAAA = createTabDto({ + id: 'AAA', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/AAA.txt') }, + editorId: 'default' + }); + + const tabDtoBBB = createTabDto({ + id: 'BBB', + isActive: false, + isDirty: true, + isPinned: true, + label: 'label1', + input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/BBB.txt') }, + editorId: 'default' + }); + + // single dirty tab + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tabDtoAAA, tabDtoBBB] + }]); + + let all = extHostEditorTabs.tabGroups.groups.map(group => group.tabs).flat(); + assert.strictEqual(all.length, 2); + + const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; + assert.ok(activeTab1?.input instanceof TextTabInput); + assert.strictEqual(tabDtoAAA.input.kind, TabInputKind.TextInput); + const dtoAAAResource = (tabDtoAAA.input as TextInputDto).uri; + assert.strictEqual(activeTab1?.input?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); + assert.strictEqual(activeTab1?.isActive, true); + + extHostEditorTabs.$acceptTabUpdate(12, { ...tabDtoBBB, isActive: true }); /// BBB is now active + + const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; + assert.ok(activeTab2?.input instanceof TextTabInput); + assert.strictEqual(tabDtoBBB.input.kind, TabInputKind.TextInput); + const dtoBBBResource = (tabDtoBBB.input as TextInputDto).uri; + assert.strictEqual(activeTab2?.input?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); + assert.strictEqual(activeTab2?.isActive, true); + assert.strictEqual(activeTab1?.isActive, false); + }); + + test('vscode.window.tagGroups is immutable', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + assert.throws(() => { + // @ts-expect-error write to readonly prop + extHostEditorTabs.tabGroups.activeTabGroup = undefined; + }); + assert.throws(() => { + // @ts-expect-error write to readonly prop + extHostEditorTabs.tabGroups.groups.length = 0; + }); + assert.throws(() => { + // @ts-expect-error write to readonly prop + extHostEditorTabs.tabGroups.onDidChangeActiveTabGroup = undefined; + }); + assert.throws(() => { + // @ts-expect-error write to readonly prop + extHostEditorTabs.tabGroups.onDidChangeTabGroup = undefined; + }); + }); + + test('Ensure close is called with all tab ids', function () { + let closedTabIds: string[][] = []; + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + override async $closeTab(tabIds: string[], preserveFocus?: boolean) { + closedTabIds.push(tabIds); + } + }) + ); + const tab: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + editorId: 'default' + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + const activeTab = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; + assert.ok(activeTab); + extHostEditorTabs.tabGroups.close(activeTab, false); + assert.strictEqual(closedTabIds.length, 1); + assert.deepStrictEqual(closedTabIds[0], ['uniquestring']); + // Close with array + extHostEditorTabs.tabGroups.close([activeTab], false); + assert.strictEqual(closedTabIds.length, 2); + assert.deepStrictEqual(closedTabIds[1], ['uniquestring']); + }); + + test('Update tab only sends tab change event', async function () { + let closedTabIds: string[][] = []; + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + override async $closeTab(tabIds: string[], preserveFocus?: boolean) { + closedTabIds.push(tabIds); + } + }) + ); + const tabDto: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + editorId: 'default' + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tabDto] + }]); + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + + const tab = extHostEditorTabs.tabGroups.groups[0].tabs[0]; + + const p = new Promise(resolve => extHostEditorTabs.tabGroups.onDidChangeTab(resolve)); + + extHostEditorTabs.$acceptTabUpdate(12, { ...tabDto, label: 'NEW LABEL' }); + + const changedTab = await p; + + assert.ok(tab === changedTab); + assert.strictEqual(changedTab.label, 'NEW LABEL'); + + }); + + test('Active tab', function () { + + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + const tab1: IEditorTabDto = createTabDto({ + id: 'uniquestring', + isActive: true, + isDirty: true, + isPinned: true, + label: 'label1', + }); + + const tab2: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring2', + }); + + const tab3: IEditorTabDto = createTabDto({ + isActive: false, + id: 'uniquestring3', + }); + + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab1, tab2, tab3] + }]); + + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 3); + + // Active tab is correct + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); + + // Switching active tab works + tab1.isActive = false; + tab2.isActive = true; + extHostEditorTabs.$acceptTabUpdate(12, tab1); + extHostEditorTabs.$acceptTabUpdate(12, tab2); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[1]); + + //Closing tabs out works + tab3.isActive = true; + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [tab3] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]); + + // Closing out all tabs returns undefine active tab + extHostEditorTabs.$acceptEditorTabModel([{ + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }]); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.length, 1); + assert.strictEqual(extHostEditorTabs.tabGroups.groups.map(g => g.tabs).flat().length, 0); + assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, undefined); + }); +}); diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 9930ad289b2..aa96537670d 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -24,6 +24,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { Event } from 'vs/base/common/event'; import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; +import { VSBuffer } from 'vs/base/common/buffer'; suite('NotebookCell#Document', function () { @@ -418,4 +419,78 @@ suite('NotebookCell#Document', function () { assert.strictEqual(first.document.languageId, 'fooLang'); assert.ok(removedDoc === addedDoc); }); + + test('onDidChangeNotebook-event, cell changes', async function () { + + const p = Event.toPromise(extHostNotebookDocuments.onDidChangeNotebookDocument); + + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, new SerializableObjectWithBuffers({ + versionId: 12, rawEvents: [{ + kind: NotebookCellsChangeType.ChangeCellMetadata, + index: 0, + metadata: { foo: 1 } + }, { + kind: NotebookCellsChangeType.ChangeCellMetadata, + index: 1, + metadata: { foo: 2 }, + }, { + kind: NotebookCellsChangeType.Output, + index: 1, + outputs: [ + { + items: [{ + valueBytes: VSBuffer.fromByteArray([0, 2, 3]), + mime: 'text/plain' + }], + outputId: '1' + } + ] + }] + }), false, undefined); + + + const event = await p; + + assert.strictEqual(event.notebook === notebook.apiNotebook, true); + assert.strictEqual(event.contentChanges.length, 0); + assert.strictEqual(event.cellChanges.length, 2); + + const [first, second] = event.cellChanges; + assert.deepStrictEqual(first.metadata, first.cell.metadata); + assert.deepStrictEqual(first.executionSummary, undefined); + assert.deepStrictEqual(first.outputs, undefined); + + assert.deepStrictEqual(second.outputs, second.cell.outputs); + assert.deepStrictEqual(second.metadata, second.cell.metadata); + assert.deepStrictEqual(second.executionSummary, undefined); + }); + + test('onDidChangeNotebook-event, notebook metadata', async function () { + + const p = Event.toPromise(extHostNotebookDocuments.onDidChangeNotebookDocument); + + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, new SerializableObjectWithBuffers({ versionId: 12, rawEvents: [] }), false, { foo: 2 }); + + const event = await p; + + assert.strictEqual(event.notebook === notebook.apiNotebook, true); + assert.strictEqual(event.contentChanges.length, 0); + assert.strictEqual(event.cellChanges.length, 0); + assert.deepStrictEqual(event.metadata, { foo: 2 }); + }); + + test('onDidChangeNotebook-event, froozen data', async function () { + + const p = Event.toPromise(extHostNotebookDocuments.onDidChangeNotebookDocument); + + extHostNotebookDocuments.$acceptModelChanged(notebook.uri, new SerializableObjectWithBuffers({ versionId: 12, rawEvents: [] }), false, { foo: 2 }); + + const event = await p; + + assert.ok(Object.isFrozen(event)); + assert.ok(Object.isFrozen(event.cellChanges)); + assert.ok(Object.isFrozen(event.contentChanges)); + assert.ok(Object.isFrozen(event.notebook)); + assert.ok(!Object.isFrozen(event.metadata)); + }); }); diff --git a/src/vs/workbench/api/test/browser/extHostTesting.test.ts b/src/vs/workbench/api/test/browser/extHostTesting.test.ts index 3a88b6731cb..85cfba00a3f 100644 --- a/src/vs/workbench/api/test/browser/extHostTesting.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTesting.test.ts @@ -473,7 +473,7 @@ suite('ExtHost Testing', () => { cts = new CancellationTokenSource(); c = new TestRunCoordinator(proxy); - configuration = new TestRunProfileImpl(mockObject()(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); + configuration = new TestRunProfileImpl(mockObject()(), new Map(), 'ctrlId', 42, 'Do Run', TestRunProfileKind.Run, () => { }, false); await single.expand(single.root.id, Infinity); single.collectDiff(); @@ -527,7 +527,7 @@ suite('ExtHost Testing', () => { controllerId: 'ctrl', id: tracker.id, include: [single.root.id], - exclude: ['id-b'], + exclude: [new TestId(['ctrlId', 'id-b']).toString()], persist: false, }] ]); diff --git a/src/vs/workbench/api/test/browser/extHostTypes.test.ts b/src/vs/workbench/api/test/browser/extHostTypes.test.ts index a82c5998ef6..bcb92a1d701 100644 --- a/src/vs/workbench/api/test/browser/extHostTypes.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypes.test.ts @@ -679,7 +679,7 @@ suite('ExtHostTypes', function () { // --- JSON item = types.NotebookCellOutputItem.json(1); - assert.strictEqual(item.mime, 'application/json'); + assert.strictEqual(item.mime, 'text/x-json'); assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(1))); item = types.NotebookCellOutputItem.json(1, 'foo/bar'); @@ -687,11 +687,11 @@ suite('ExtHostTypes', function () { assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(1))); item = types.NotebookCellOutputItem.json(true); - assert.strictEqual(item.mime, 'application/json'); + assert.strictEqual(item.mime, 'text/x-json'); assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify(true))); item = types.NotebookCellOutputItem.json([true, 1, 'ddd']); - assert.strictEqual(item.mime, 'application/json'); + assert.strictEqual(item.mime, 'text/x-json'); assert.deepStrictEqual(item.data, new TextEncoder().encode(JSON.stringify([true, 1, 'ddd'], undefined, '\t'))); // --- text diff --git a/src/vs/workbench/api/test/browser/extHostWebview.test.ts b/src/vs/workbench/api/test/browser/extHostWebview.test.ts index d47cfdd98bc..a8db2073176 100644 --- a/src/vs/workbench/api/test/browser/extHostWebview.test.ts +++ b/src/vs/workbench/api/test/browser/extHostWebview.test.ts @@ -32,7 +32,7 @@ suite('ExtHostWebview', () => { test('Cannot register multiple serializers for the same view type', async () => { const viewType = 'view.type'; - const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { remote: { authority: undefined, isRemote: false } }, undefined, new NullLogService(), NullApiDeprecationService); + const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { authority: undefined, isRemote: false }, undefined, new NullLogService(), NullApiDeprecationService); const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); @@ -169,10 +169,8 @@ suite('ExtHostWebview', () => { function createWebview(rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined, remoteAuthority: string | undefined) { const extHostWebviews = new ExtHostWebviews(rpcProtocol!, { - remote: { - authority: remoteAuthority, - isRemote: !!remoteAuthority, - }, + authority: remoteAuthority, + isRemote: !!remoteAuthority, }, undefined, new NullLogService(), NullApiDeprecationService); const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); diff --git a/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts new file mode 100644 index 00000000000..ddaaa76bb35 --- /dev/null +++ b/src/vs/workbench/api/test/common/extHostExtensionActivator.test.ts @@ -0,0 +1,269 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { timeout } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionsActivator, IExtensionsActivatorHost } from 'vs/workbench/api/common/extHostExtensionActivator'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; + +suite('ExtensionsActivator', () => { + + const idA = new ExtensionIdentifier(`a`); + const idB = new ExtensionIdentifier(`b`); + const idC = new ExtensionIdentifier(`c`); + + test('calls activate only once with sequential activations', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA) + ]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('calls activate only once with parallel activations', async () => { + const extActivation = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivation] + ]); + const activator = createActivator(host, [ + desc(idA, [], ['evt1', 'evt2']) + ]); + + const activate1 = activator.activateByEvent('evt1', false); + const activate2 = activator.activateByEvent('evt2', false); + + extActivation.resolve(); + + await activate1; + await activate2; + + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('activates dependencies first', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB], ['evt1']), + desc(idB, [], ['evt1']), + ]); + + const activate = activator.activateByEvent('evt1', false); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB]); + extActivationB.resolve(); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + extActivationA.resolve(); + + await timeout(0); + await activate; + + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + }); + + test('Supports having resolved extensions', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA, [idB]) + ], [idB]); + + await activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idA]); + }); + + test('Supports having external extensions', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB]) + ], [], [idB]); + + const activate = activator.activateByEvent('*', false); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB]); + extActivationB.resolve(); + + await timeout(0); + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + extActivationA.resolve(); + + await activate; + assert.deepStrictEqual(host.activateCalls, [idB, idA]); + }); + + test('Error: activateById with missing extension', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA), + desc(idB), + ]); + + let error: Error | undefined = undefined; + try { + await activator.activateById(idC, { startup: false, extensionId: idC, activationEvent: 'none' }); + } catch (err) { + error = err; + } + + assert.strictEqual(typeof error === 'undefined', false); + }); + + test('Error: dependency missing', async () => { + const host = new SimpleExtensionsActivatorHost(); + const activator = createActivator(host, [ + desc(idA, [idB]), + ]); + + await activator.activateByEvent('*', false); + + assert.deepStrictEqual(host.errors.length, 1); + assert.deepStrictEqual(host.errors[0][0], idA); + }); + + test('Error: dependency activation failed', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB] + ]); + const activator = createActivator(host, [ + desc(idA, [idB]), + desc(idB) + ]); + + const activate = activator.activateByEvent('*', false); + extActivationB.reject(new Error(`b fails!`)); + + await activate; + assert.deepStrictEqual(host.errors.length, 2); + assert.deepStrictEqual(host.errors[0][0], idB); + assert.deepStrictEqual(host.errors[1][0], idA); + }); + + test('issue #144518: Problem with git extension and vscode-icons', async () => { + const extActivationA = new ExtensionActivationPromiseSource(); + const extActivationB = new ExtensionActivationPromiseSource(); + const extActivationC = new ExtensionActivationPromiseSource(); + const host = new PromiseExtensionsActivatorHost([ + [idA, extActivationA], + [idB, extActivationB], + [idC, extActivationC] + ]); + const activator = createActivator(host, [ + desc(idA, [idB]), + desc(idB), + desc(idC), + ]); + + activator.activateByEvent('*', false); + assert.deepStrictEqual(host.activateCalls, [idB, idC]); + + extActivationB.resolve(); + await timeout(0); + + assert.deepStrictEqual(host.activateCalls, [idB, idC, idA]); + extActivationA.resolve(); + }); + + class SimpleExtensionsActivatorHost implements IExtensionsActivatorHost { + public readonly activateCalls: ExtensionIdentifier[] = []; + public readonly errors: [ExtensionIdentifier, Error | null, MissingExtensionDependency | null][] = []; + + onExtensionActivationError(extensionId: ExtensionIdentifier, error: Error | null, missingExtensionDependency: MissingExtensionDependency | null): void { + this.errors.push([extensionId, error, missingExtensionDependency]); + } + + actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { + this.activateCalls.push(extensionId); + return Promise.resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); + } + } + + class PromiseExtensionsActivatorHost extends SimpleExtensionsActivatorHost { + + constructor( + private readonly _promises: [ExtensionIdentifier, ExtensionActivationPromiseSource][] + ) { + super(); + } + + override actualActivateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { + this.activateCalls.push(extensionId); + for (const [id, promiseSource] of this._promises) { + if (id.value === extensionId.value) { + return promiseSource.promise; + } + } + throw new Error(`Unexpected!`); + } + } + + class ExtensionActivationPromiseSource { + private _resolve!: (value: ActivatedExtension) => void; + private _reject!: (err: Error) => void; + public readonly promise: Promise; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + public resolve(): void { + this._resolve(new EmptyExtension(ExtensionActivationTimes.NONE)); + } + + public reject(err: Error): void { + this._reject(err); + } + } + + function createActivator(host: IExtensionsActivatorHost, extensionDescriptions: IExtensionDescription[], resolvedExtensions: ExtensionIdentifier[] = [], hostExtensions: ExtensionIdentifier[] = []): ExtensionsActivator { + const registry = new ExtensionDescriptionRegistry(extensionDescriptions); + return new ExtensionsActivator(registry, resolvedExtensions, hostExtensions, host, new NullLogService()); + } + + function desc(id: ExtensionIdentifier, deps: ExtensionIdentifier[] = [], activationEvents: string[] = ['*']): IExtensionDescription { + return { + name: id.value, + publisher: 'test', + version: '0.0.0', + engines: { vscode: '^1.0.0' }, + identifier: id, + extensionLocation: URI.parse(`nothing://nowhere`), + isBuiltin: false, + isUnderDevelopment: false, + isUserBuiltin: false, + activationEvents, + main: 'index.js', + targetPlatform: TargetPlatform.UNDEFINED, + extensionDependencies: deps.map(d => d.value) + }; + } + +}); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 20abdb48883..f1b97346573 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -33,8 +33,8 @@ import { ICommandActionTitle } from 'vs/platform/action/common/action'; const menubarIcon = registerIcon('menuBar', Codicon.layoutMenubar, localize('menuBarIcon', "Represents the menu bar")); const activityBarLeftIcon = registerIcon('activity-bar-left', Codicon.layoutActivitybarLeft, localize('activityBarLeft', "Represents the activity bar in the left position")); const activityBarRightIcon = registerIcon('activity-bar-right', Codicon.layoutActivitybarRight, localize('activityBarRight', "Represents the activity bar in the right position")); -const panelLeftIcon = registerIcon('panel-left', Codicon.layoutSidebarLeft, localize('panelLeft', "Represents the side bar or side panel in the left position")); -const panelRightIcon = registerIcon('panel-right', Codicon.layoutSidebarRight, localize('panelRight', "Represents the side bar or side panel in the right position")); +const panelLeftIcon = registerIcon('panel-left', Codicon.layoutSidebarLeft, localize('panelLeft', "Represents a side bar in the left position")); +const panelRightIcon = registerIcon('panel-right', Codicon.layoutSidebarRight, localize('panelRight', "Represents side bar in the right position")); const panelIcon = registerIcon('panel-bottom', Codicon.layoutPanel, localize('panelBottom', "Represents the bottom panel")); const statusBarIcon = registerIcon('statusBar', Codicon.layoutStatusbar, localize('statusBarIcon', "Represents the status bar")); @@ -55,7 +55,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.closeSidebar', - title: { value: localize('closeSidebar', "Close Side Bar"), original: 'Close Side Bar' }, + title: { value: localize('closeSidebar', "Close Primary Side Bar"), original: 'Close Primary Side Bar' }, category: CATEGORIES.View, f1: true }); @@ -164,8 +164,8 @@ class MoveSidebarRightAction extends MoveSidebarPositionAction { constructor() { super(MoveSidebarRightAction.ID, { - value: localize('moveSidebarRight', "Move Side Bar Right"), - original: 'Move Side Bar Right' + value: localize('moveSidebarRight', "Move Primary Side Bar Right"), + original: 'Move Primary Side Bar Right' }, Position.RIGHT); } } @@ -175,8 +175,8 @@ class MoveSidebarLeftAction extends MoveSidebarPositionAction { constructor() { super(MoveSidebarLeftAction.ID, { - value: localize('moveSidebarLeft', "Move Side Bar Left"), - original: 'Move Side Bar Left' + value: localize('moveSidebarLeft', "Move Primary Side Bar Left"), + original: 'Move Primary Side Bar Left' }, Position.LEFT); } } @@ -189,16 +189,16 @@ registerAction2(MoveSidebarLeftAction); export class ToggleSidebarPositionAction extends Action2 { static readonly ID = 'workbench.action.toggleSidebarPosition'; - static readonly LABEL = localize('toggleSidebarPosition', "Toggle Side Bar Position"); + static readonly LABEL = localize('toggleSidebarPosition', "Toggle Primary Side Bar Position"); static getLabel(layoutService: IWorkbenchLayoutService): string { - return layoutService.getSideBarPosition() === Position.LEFT ? localize('moveSidebarRight', "Move Side Bar Right") : localize('moveSidebarLeft', "Move Side Bar Left"); + return layoutService.getSideBarPosition() === Position.LEFT ? localize('moveSidebarRight', "Move Primary Side Bar Right") : localize('moveSidebarLeft', "Move Primary Side Bar Left"); } constructor() { super({ id: ToggleSidebarPositionAction.ID, - title: { value: localize('toggleSidebarPosition', "Toggle Side Bar Position"), original: 'Toggle Side Bar Position' }, + title: { value: localize('toggleSidebarPosition', "Toggle Primary Side Bar Position"), original: 'Toggle Primary Side Bar Position' }, category: CATEGORIES.View, f1: true }); @@ -223,7 +223,7 @@ MenuRegistry.appendMenuItem(MenuId.LayoutControlMenu, { title: localize('configureLayout', "Configure Layout"), icon: configureLayoutIcon, group: '1_workbench_layout', - when: ContextKeyExpr.or(ContextKeyExpr.equals('config.workbench.experimental.layoutControl.type', 'menu'), ContextKeyExpr.equals('config.workbench.experimental.layoutControl.type', 'both')) + when: ContextKeyExpr.equals('config.workbench.experimental.layoutControl.type', 'menu') }); @@ -233,7 +233,7 @@ MenuRegistry.appendMenuItems([{ group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar right', "Move Side Bar Right") + title: localize('move side bar right', "Move Primary Side Bar Right") }, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 1 @@ -244,7 +244,7 @@ MenuRegistry.appendMenuItems([{ group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar right', "Move Side Bar Right") + title: localize('move sidebar right', "Move Primary Side Bar Right") }, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 1 @@ -255,7 +255,7 @@ MenuRegistry.appendMenuItems([{ group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar left', "Move Side Bar Left") + title: localize('move sidebar left', "Move Primary Side Bar Left") }, when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 1 @@ -266,7 +266,7 @@ MenuRegistry.appendMenuItems([{ group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize('move sidebar left', "Move Side Bar Left") + title: localize('move sidebar left', "Move Primary Side Bar Left") }, when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))), order: 1 @@ -277,7 +277,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Right") + title: localize({ key: 'miMoveSidebarRight', comment: ['&& denotes a mnemonic'] }, "&&Move Primary Side Bar Right") }, when: ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), order: 2 @@ -287,7 +287,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { group: '3_workbench_layout_move', command: { id: ToggleSidebarPositionAction.ID, - title: localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Side Bar Left") + title: localize({ key: 'miMoveSidebarLeft', comment: ['&& denotes a mnemonic'] }, "&&Move Primary Side Bar Left") }, when: ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), order: 2 @@ -338,7 +338,7 @@ class ToggleSidebarVisibilityAction extends Action2 { constructor() { super({ id: ToggleSidebarVisibilityAction.ID, - title: { value: localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' }, + title: { value: localize('toggleSidebar', "Toggle Primary Side Bar Visibility"), original: 'Toggle Primary Side Bar Visibility' }, category: CATEGORIES.View, f1: true, keybinding: { @@ -386,7 +386,7 @@ MenuRegistry.appendMenuItems([ group: '2_workbench_layout', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"), + title: localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Primary Side Bar"), toggled: SideBarVisibleContext }, order: 1 @@ -397,7 +397,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_layout', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize('miShowSidebarNoMnnemonic', "Show Side Bar"), + title: localize('miShowSidebarNoMnnemonic', "Show Primary Side Bar"), toggled: SideBarVisibleContext }, order: 0 @@ -408,7 +408,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_toggles', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize('toggleSideBar', "Toggle Side Bar"), + title: localize('toggleSideBar', "Toggle Primary Side Bar"), icon: panelLeftIcon, toggled: SideBarVisibleContext }, @@ -421,7 +421,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_toggles', command: { id: ToggleSidebarVisibilityAction.ID, - title: localize('toggleSideBar', "Toggle Side Bar"), + title: localize('toggleSideBar', "Toggle Primary Side Bar"), icon: panelRightIcon, toggled: SideBarVisibleContext }, @@ -540,7 +540,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.EditorContrib - 1000, handler(accessor: ServicesAccessor) { const layoutService = accessor.get(IWorkbenchLayoutService); - layoutService.toggleZenMode(); + const contextKeyService = accessor.get(IContextKeyService); + if (InEditorZenModeContext.getValue(contextKeyService)) { + layoutService.toggleZenMode(); + } }, when: InEditorZenModeContext, primary: KeyChord(KeyCode.Escape, KeyCode.Escape) @@ -701,7 +704,7 @@ registerAction2(class extends Action2 { if (!hasAddedView) { results.push({ type: 'separator', - label: localize('sidePanelContainer', "Side Panel / {0}", containerModel.title) + label: localize('secondarySideBarContainer', "Secondary Side Bar / {0}", containerModel.title) }); hasAddedView = true; } @@ -806,7 +809,7 @@ class MoveFocusedViewAction extends Action2 { if (!(isViewSolo && currentLocation === ViewContainerLocation.AuxiliaryBar)) { items.push({ id: '_.auxiliarybar.newcontainer', - label: localize('moveFocusedView.newContainerInSidePanel', "New Side Panel Entry") + label: localize('moveFocusedView.newContainerInSidePanel', "New Secondary Side Bar Entry") }); } @@ -854,7 +857,7 @@ class MoveFocusedViewAction extends Action2 { items.push({ type: 'separator', - label: localize('sidePanel', "Side Panel") + label: localize('secondarySideBar', "Secondary Side Bar") }); const pinnedAuxPanels = paneCompositePartService.getPinnedPaneCompositeIds(ViewContainerLocation.AuxiliaryBar); @@ -1130,9 +1133,9 @@ if (!isMacintosh || !isNative) { ToggleVisibilityActions.push(...[ CreateToggleLayoutItem(ToggleActivityBarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.activityBar.visible', true), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }), - CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }), + CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }), + CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }), CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon), - CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('sidePanel', "Side Panel"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }), CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon), ]); @@ -1167,10 +1170,16 @@ registerAction2(class CustomizeLayoutAction extends Action2 { id: 'workbench.action.customizeLayout', title: localize('customizeLayout', "Customize Layout..."), f1: true, + icon: configureLayoutIcon, menu: [ { id: MenuId.LayoutControlMenuSubmenu, group: 'z_end', + }, + { + id: MenuId.LayoutControlMenu, + when: ContextKeyExpr.equals('config.workbench.experimental.layoutControl.type', 'both'), + group: 'z_end' } ] }); @@ -1217,7 +1226,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 { ...ToggleVisibilityActions.map(toQuickPickItem), { type: 'separator', - label: localize('sideBarPosition', "Side Bar Position") + label: localize('sideBarPosition', "Primary Side Bar Position") }, ...MoveSideBarActions.map(toQuickPickItem), { diff --git a/src/vs/workbench/browser/codeeditor.ts b/src/vs/workbench/browser/codeeditor.ts index bb0e244ca06..bc251d1bda1 100644 --- a/src/vs/workbench/browser/codeeditor.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -13,7 +13,7 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { buttonBackground, buttonForeground, editorBackground, editorForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { hasWorkspaceFileExtension, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { hasWorkspaceFileExtension, isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -258,6 +258,10 @@ export class OpenWorkspaceButtonContribution extends Disposable implements IEdit return false; // needs to be backed by a file service } + if (isTemporaryWorkspace(this.contextService.getWorkspace())) { + return false; // unsupported in temporary workspaces + } + if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { const workspaceConfiguration = this.contextService.getWorkspace().configuration; if (workspaceConfiguration && isEqual(workspaceConfiguration, model.uri)) { diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 899838b04e2..a43d0472e25 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -23,7 +23,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IEditorIdentifier, GroupIdentifier, isEditorIdentifier, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { addDisposableListener, EventType } from 'vs/base/browser/dom'; +import { addDisposableListener, DragAndDropObserver, EventType } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -31,9 +31,9 @@ import { Emitter } from 'vs/base/common/event'; import { coalesce } from 'vs/base/common/arrays'; import { parse, stringify } from 'vs/base/common/marshalling'; import { ILabelService } from 'vs/platform/label/common/label'; -import { hasWorkspaceFileExtension, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { hasWorkspaceFileExtension, isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { ITreeDataTransfer } from 'vs/workbench/common/views'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; import { extractSelection } from 'vs/platform/opener/common/opener'; import { IListDragAndDrop } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; @@ -75,8 +75,8 @@ export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInpu /** * Whether we probe for the dropped editor to be a workspace - * allowing to open it as workspace instead of opening as - * editor. + * (i.e. code-workspace file or even a folder), allowing to + * open it as workspace instead of opening as editor. */ allowWorkspaceOpen?: boolean; } @@ -177,7 +177,7 @@ function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | return editors; } -export async function extractTreeDropData(dataTransfer: ITreeDataTransfer): Promise> { +export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise> { const editors: IDraggedResourceEditorInput[] = []; const resourcesKey = Mimes.uriList.toLowerCase(); @@ -313,8 +313,9 @@ export async function extractFileListData(accessor: ServicesAccessor, files: Fil export interface IResourcesDropHandlerOptions { /** - * Whether to open the actual workspace when a workspace configuration file is dropped - * or whether to open the configuration file within the editor as normal file. + * Whether we probe for the dropped resource to be a workspace + * (i.e. code-workspace file or even a folder), allowing to + * open it as workspace instead of opening as editor. */ readonly allowWorkspaceOpen: boolean; } @@ -351,7 +352,7 @@ export class ResourcesDropHandler { if (this.options.allowWorkspaceOpen) { const localFilesAllowedToOpenAsWorkspace = coalesce(editors.filter(editor => editor.allowWorkspaceOpen && editor.resource?.scheme === Schemas.file).map(editor => editor.resource)); if (localFilesAllowedToOpenAsWorkspace.length > 0) { - const isWorkspaceOpening = await this.handleWorkspaceFileDrop(localFilesAllowedToOpenAsWorkspace); + const isWorkspaceOpening = await this.handleWorkspaceDrop(localFilesAllowedToOpenAsWorkspace); if (isWorkspaceOpening) { return; // return early if the drop operation resulted in this window changing to a workspace } @@ -384,7 +385,7 @@ export class ResourcesDropHandler { afterDrop(targetGroup); } - private async handleWorkspaceFileDrop(resources: URI[]): Promise { + private async handleWorkspaceDrop(resources: URI[]): Promise { const toOpen: IWindowOpenable[] = []; const folderURIs: IWorkspaceFolderCreationData[] = []; @@ -422,7 +423,12 @@ export class ResourcesDropHandler { await this.hostService.openWindow(toOpen); } - // folders.length > 1: Multiple folders: Create new workspace with folders and open + // Add to workspace if we are in a temporary workspace + else if (isTemporaryWorkspace(this.contextService.getWorkspace())) { + await this.workspaceEditingService.addFolders(folderURIs); + } + + // Finaly, enter untitled workspace when dropping >1 folders else { await this.workspaceEditingService.createAndEnterWorkspace(folderURIs); } @@ -626,64 +632,6 @@ export class LocalSelectionTransfer { } } -export interface IDragAndDropObserverCallbacks { - readonly onDragEnter: (e: DragEvent) => void; - readonly onDragLeave: (e: DragEvent) => void; - readonly onDrop: (e: DragEvent) => void; - readonly onDragEnd: (e: DragEvent) => void; - - readonly onDragOver?: (e: DragEvent) => void; -} - -export class DragAndDropObserver extends Disposable { - - // A helper to fix issues with repeated DRAG_ENTER / DRAG_LEAVE - // calls see https://github.com/microsoft/vscode/issues/14470 - // when the element has child elements where the events are fired - // repeadedly. - private counter: number = 0; - - constructor(private readonly element: HTMLElement, private readonly callbacks: IDragAndDropObserverCallbacks) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(addDisposableListener(this.element, EventType.DRAG_ENTER, (e: DragEvent) => { - this.counter++; - - this.callbacks.onDragEnter(e); - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_OVER, (e: DragEvent) => { - e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome) - - if (this.callbacks.onDragOver) { - this.callbacks.onDragOver(e); - } - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_LEAVE, (e: DragEvent) => { - this.counter--; - - if (this.counter === 0) { - this.callbacks.onDragLeave(e); - } - })); - - this._register(addDisposableListener(this.element, EventType.DRAG_END, (e: DragEvent) => { - this.counter = 0; - this.callbacks.onDragEnd(e); - })); - - this._register(addDisposableListener(this.element, EventType.DROP, (e: DragEvent) => { - this.counter = 0; - this.callbacks.onDrop(e); - })); - } -} - export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean { if (!event.dataTransfer) { return false; diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index f8b2205ca2a..1bf6c4750e2 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -379,7 +379,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi sideBar.updateStyles(); auxiliaryBar.updateStyles(); - // Move activity bar, side bar, and side panel + // Move activity bar and side bars this.adjustPartPositions(position, panelAlignment, panelPosition); } @@ -1484,7 +1484,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private adjustPartPositions(sideBarPosition: Position, panelAlignment: PanelAlignment, panelPosition: Position): void { - // Move activity bar, side bar, and side panel + // Move activity bar and side bars const sideBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.LEFT && panelAlignment === 'right') || (sideBarPosition === Position.RIGHT && panelAlignment === 'left')); const auxiliaryBarSiblingToEditor = panelPosition !== Position.BOTTOM || !(panelAlignment === 'center' || (sideBarPosition === Position.RIGHT && panelAlignment === 'right') || (sideBarPosition === Position.LEFT && panelAlignment === 'left')); const preMovePanelWidth = !this.isVisible(Parts.PANEL_PART) ? Sizing.Invisible(this.workbenchGrid.getViewCachedVisibleSize(this.panelPartView) ?? this.panelPartView.minimumWidth) : this.workbenchGrid.getViewSize(this.panelPartView).width; diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 4568fa4a591..36647e6dcfe 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -371,7 +371,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.previousSideBarView', - title: { value: localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' }, + title: { value: localize('previousSideBarView', "Previous Primary Side Bar View"), original: 'Previous Primary Side Bar View' }, category: CATEGORIES.View, f1: true }, -1); @@ -384,7 +384,7 @@ registerAction2( constructor() { super({ id: 'workbench.action.nextSideBarView', - title: { value: localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' }, + title: { value: localize('nextSideBarView', "Next Primary Side Bar View"), original: 'Next Primary Side Bar View' }, category: CATEGORIES.View, f1: true }, 1); @@ -467,8 +467,8 @@ registerThemingParticipant((theme, collector) => { .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:before { content: ""; position: absolute; - top: 9px; - left: 9px; + top: 8px; + left: 8px; height: 32px; width: 32px; z-index: 1; diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index d0a2aefcb34..453b2041417 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -103,7 +103,8 @@ /* Hides active elements in high contrast mode */ -.monaco-workbench.hc-black .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator { +.monaco-workbench.hc-black .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator.action-item, +.monaco-workbench.hc-light .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked:not(:focus) .active-item-indicator { display: none; } @@ -121,7 +122,9 @@ /* Hides outline on HC as focus is handled by border */ .monaco-workbench.hc-black .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, -.monaco-workbench.hc-black .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { +.monaco-workbench.hc-black .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, +.monaco-workbench.hc-light .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, +.monaco-workbench.hc-light .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { outline: none; } diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts index 2bfd75de107..64dfa1da6d1 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions.ts @@ -23,7 +23,7 @@ const auxiliaryBarLeftIcon = registerIcon('auxiliarybar-left-layout-icon', Codic export class ToggleAuxiliaryBarAction extends Action { static readonly ID = 'workbench.action.toggleAuxiliaryBar'; - static readonly LABEL = localize('toggleAuxiliaryBar', "Toggle Side Panel"); + static readonly LABEL = localize('toggleAuxiliaryBar', "Toggle Secondary Side Bar"); constructor( id: string, @@ -41,7 +41,7 @@ export class ToggleAuxiliaryBarAction extends Action { class FocusAuxiliaryBarAction extends Action { static readonly ID = 'workbench.action.focusAuxiliaryBar'; - static readonly LABEL = localize('focusAuxiliaryBar', "Focus into Side Panel"); + static readonly LABEL = localize('focusAuxiliaryBar', "Focus into Secondary Side Bar"); constructor( id: string, @@ -74,10 +74,10 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_layout', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize('miShowAuxiliaryBarNoMnemonic', "Show Side Panel"), + title: localize('miShowAuxiliaryBarNoMnemonic', "Show Secondary Side Bar"), toggled: AuxiliaryBarVisibleContext }, - order: 4 + order: 2 } }, { @@ -86,7 +86,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_toggles', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize('toggleSidePanel', "Toggle Side Panel"), + title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"), toggled: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon, }, @@ -100,7 +100,7 @@ MenuRegistry.appendMenuItems([ group: '0_workbench_toggles', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize('toggleSidePanel', "Toggle Side Panel"), + title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"), toggled: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon, }, @@ -114,10 +114,10 @@ MenuRegistry.appendMenuItems([ group: '2_workbench_layout', command: { id: ToggleAuxiliaryBarAction.ID, - title: localize({ key: 'miShowAuxiliaryBar', comment: ['&& denotes a mnemonic'] }, "Show Si&&de Panel"), + title: localize({ key: 'miShowAuxiliaryBar', comment: ['&& denotes a mnemonic'] }, "Show Secondary Si&&de Bar"), toggled: AuxiliaryBarVisibleContext }, - order: 5 + order: 2 } }, { id: MenuId.ViewTitleContext, @@ -125,7 +125,7 @@ MenuRegistry.appendMenuItems([ group: '3_workbench_layout_move', command: { id: ToggleAuxiliaryBarAction.ID, - title: { value: localize('hideAuxiliaryBar', "Hide Side Panel"), original: 'Hide Side Panel' }, + title: { value: localize('hideAuxiliaryBar', "Hide Secondary Side Bar"), original: 'Hide Secondary Side Bar' }, }, when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))), order: 2 @@ -134,5 +134,5 @@ MenuRegistry.appendMenuItems([ ]); const actionRegistry = Registry.as(WorkbenchExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleAuxiliaryBarAction), 'View: Toggle Side Panel', CATEGORIES.View.value); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusAuxiliaryBarAction), 'View: Focus into Side Panel', CATEGORIES.View.value); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleAuxiliaryBarAction), 'View: Toggle Secondary Side Bar', CATEGORIES.View.value); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusAuxiliaryBarAction), 'View: Focus into Secondary Side Bar', CATEGORIES.View.value); diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index e90911a75dc..7534ee7e9b4 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -23,10 +23,9 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IActivityHoverOptions } from 'vs/workbench/browser/parts/compositeBarActions'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; -import { IAction, Separator, toAction } from 'vs/base/common/actions'; +import { IAction, Separator } from 'vs/base/common/actions'; import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { assertIsDefined } from 'vs/base/common/types'; -import { MoveSidePanelToPanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { LayoutPriority } from 'vs/base/browser/ui/splitview/splitview'; export class AuxiliaryBarPart extends BasePanelPart { @@ -110,8 +109,7 @@ export class AuxiliaryBarPart extends BasePanelPart { protected fillExtraContextMenuActions(actions: IAction[]): void { actions.push(...[ new Separator(), - toAction({ id: MoveSidePanelToPanelAction.ID, label: localize('moveToPanel', "Move Views to Panel"), run: () => this.instantiationService.invokeFunction(accessor => new MoveSidePanelToPanelAction().run(accessor)) }), - this.instantiationService.createInstance(ToggleAuxiliaryBarAction, ToggleAuxiliaryBarAction.ID, localize('hideAuxiliaryBar', "Hide Side Panel")) + this.instantiationService.createInstance(ToggleAuxiliaryBarAction, ToggleAuxiliaryBarAction.ID, localize('hideAuxiliaryBar', "Hide Secondary Side Bar")) ]); } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 3e853a32ef1..3bfc0a6eca2 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -417,7 +417,8 @@ export class ActivityActionViewItem extends BaseActionViewItem { content: this.computeTitle(), showPointer: true, compact: true, - skipFadeInAnimation + hideOnKeyDown: true, + skipFadeInAnimation, }); } diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index fc96b5aada9..ec4ff95eef9 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -87,7 +87,7 @@ export class BrowserDialogHandler implements IDialogHandler { const renderBody = customOptions ? (parent: HTMLElement) => { parent.classList.add(...(customOptions.classes || [])); - (customOptions.markdownDetails || []).forEach(markdownDetail => { + customOptions.markdownDetails?.forEach(markdownDetail => { const result = this.markdownRenderer.render(markdownDetail.markdown); parent.appendChild(result.element); result.element.classList.add(...(markdownDetail.classes || [])); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index d1097b86f2e..f2659b78c39 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -65,7 +65,7 @@ import { FileAccess } from 'vs/base/common/network'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { UntitledTextEditorInputSerializer, UntitledTextEditorWorkingCopyEditorHandler } from 'vs/workbench/services/untitled/common/untitledTextEditorHandler'; -import { DynamicEditorGroupAutoLockConfiguration } from 'vs/workbench/browser/parts/editor/editorConfiguration'; +import { DynamicEditorResolverConfigurations } from 'vs/workbench/browser/parts/editor/editorConfiguration'; //#region Editor Registrations @@ -125,7 +125,7 @@ Registry.as(EditorExtensions.EditorFactory).registerEdit Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorAutoSave, LifecyclePhase.Ready); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(EditorStatus, LifecyclePhase.Ready); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(UntitledTextEditorWorkingCopyEditorHandler, LifecyclePhase.Ready); -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DynamicEditorGroupAutoLockConfiguration, LifecyclePhase.Ready); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DynamicEditorResolverConfigurations, LifecyclePhase.Ready); registerEditorContribution(OpenWorkspaceButtonContribution.ID, OpenWorkspaceButtonContribution); diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index aa3b4855816..dc1669460a9 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -186,7 +186,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Clear any running auto save operation this.discardAutoSave(workingCopy); - this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString(), workingCopy.typeId); // Schedule new auto save const handle = setTimeout(() => { @@ -196,7 +196,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty if (workingCopy.isDirty()) { - this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(), workingCopy.typeId); workingCopy.save({ reason: SaveReason.AUTO }); } @@ -204,7 +204,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Keep in map for disposal as needed this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => { - this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString(), workingCopy.typeId); clearTimeout(handle); })); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index 16fd23b648a..3668557547d 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -554,17 +554,27 @@ function registerOpenEditorAPICommands(): void { } }); - CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, label?: string, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { + CommandsRegistry.registerCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, async function (accessor: ServicesAccessor, originalResource: UriComponents, modifiedResource: UriComponents, labelAndOrDescription?: string | { label: string; description: string }, columnAndOptions?: [EditorGroupColumn?, ITextEditorOptions?], context?: IOpenEvent) { const editorService = accessor.get(IEditorService); const editorGroupService = accessor.get(IEditorGroupsService); const [columnArg, optionsArg] = columnAndOptions ?? []; const [options, column] = mixinContext(context, optionsArg, columnArg); + let label: string | undefined = undefined; + let description: string | undefined = undefined; + if (typeof labelAndOrDescription === 'string') { + label = labelAndOrDescription; + } else if (labelAndOrDescription) { + label = labelAndOrDescription.label; + description = labelAndOrDescription.description; + } + await editorService.openEditor({ original: { resource: URI.revive(originalResource) }, modified: { resource: URI.revive(modifiedResource) }, label, + description, options }, columnToEditorGroup(editorGroupService, column)); }); @@ -961,7 +971,6 @@ function registerCloseEditorCommands() { ext: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; to: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - }; type WorkbenchEditorReopenEvent = { diff --git a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts index 07c01315c59..97af9e0e507 100644 --- a/src/vs/workbench/browser/parts/editor/editorConfiguration.ts +++ b/src/vs/workbench/browser/parts/editor/editorConfiguration.ts @@ -13,7 +13,7 @@ import { IEditorResolverService, RegisteredEditorInfo, RegisteredEditorPriority import { IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -export class DynamicEditorGroupAutoLockConfiguration extends Disposable implements IWorkbenchContribution { +export class DynamicEditorResolverConfigurations extends Disposable implements IWorkbenchContribution { private static readonly AUTO_LOCK_DEFAULT_ENABLED = new Set(['terminalEditor']); @@ -30,7 +30,9 @@ export class DynamicEditorGroupAutoLockConfiguration extends Disposable implemen ]; private configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); - private configurationNode: IConfigurationNode | undefined; + private autoLockConfigurationNode: IConfigurationNode | undefined; + private defaultBinaryEditorConfigurationNode: IConfigurationNode | undefined; + private editorAssociationsConfiguratioNnode: IConfigurationNode | undefined; constructor( @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @@ -57,26 +59,28 @@ export class DynamicEditorGroupAutoLockConfiguration extends Disposable implemen } private updateConfiguration(): void { - const editors = [...this.editorResolverService.getEditors(), ...DynamicEditorGroupAutoLockConfiguration.AUTO_LOCK_EXTRA_EDITORS]; + const lockableEditors = [...this.editorResolverService.getEditors(), ...DynamicEditorResolverConfigurations.AUTO_LOCK_EXTRA_EDITORS]; + const binaryEditorCandidates = this.editorResolverService.getEditors().filter(e => e.priority !== RegisteredEditorPriority.exclusive).map(e => e.id); // Build config from registered editors const autoLockGroupConfiguration: IJSONSchemaMap = Object.create(null); - for (const editor of editors) { + for (const editor of lockableEditors) { autoLockGroupConfiguration[editor.id] = { type: 'boolean', - default: DynamicEditorGroupAutoLockConfiguration.AUTO_LOCK_DEFAULT_ENABLED.has(editor.id), + default: DynamicEditorResolverConfigurations.AUTO_LOCK_DEFAULT_ENABLED.has(editor.id), description: editor.label }; } // Build default config too const defaultAutoLockGroupConfiguration = Object.create(null); - for (const editor of editors) { - defaultAutoLockGroupConfiguration[editor.id] = DynamicEditorGroupAutoLockConfiguration.AUTO_LOCK_DEFAULT_ENABLED.has(editor.id); + for (const editor of lockableEditors) { + defaultAutoLockGroupConfiguration[editor.id] = DynamicEditorResolverConfigurations.AUTO_LOCK_DEFAULT_ENABLED.has(editor.id); } - const oldConfigurationNode = this.configurationNode; - this.configurationNode = { + // Register settng for auto locking groups + const oldAutoLockConfigurationNode = this.autoLockConfigurationNode; + this.autoLockConfigurationNode = { ...workbenchConfigurationNodeBase, properties: { 'workbench.editor.autoLockGroups': { @@ -89,6 +93,40 @@ export class DynamicEditorGroupAutoLockConfiguration extends Disposable implemen } }; - this.configurationRegistry.updateConfigurations({ add: [this.configurationNode], remove: oldConfigurationNode ? [oldConfigurationNode] : [] }); + // Registers setting for default binary editors + const oldDefaultBinaryEditorConfigurationNode = this.defaultBinaryEditorConfigurationNode; + this.defaultBinaryEditorConfigurationNode = { + ...workbenchConfigurationNodeBase, + properties: { + 'workbench.editor.defaultBinaryEditor': { + type: 'string', + // This allows for intellisense autocompletion + enum: binaryEditorCandidates, + description: localize('workbench.editor.defaultBinaryEditor', "The default editor for files detected as binary. If undefined the user will be presented with a picker."), + } + } + }; + + // Registers setting for editorAssociations + const oldEditorAssociationsConfigurationNode = this.editorAssociationsConfiguratioNnode; + this.editorAssociationsConfiguratioNnode = { + ...workbenchConfigurationNodeBase, + properties: { + 'workbench.editorAssociations': { + type: 'object', + markdownDescription: localize('editor.editorAssociations', "Configure glob patterns to editors (e.g. `\"*.hex\": \"hexEditor.hexEdit\"`). These have precedence over the default behavior."), + patternProperties: { + '.*': { + type: 'string', + enum: binaryEditorCandidates, + } + } + } + } + }; + + this.configurationRegistry.updateConfigurations({ add: [this.autoLockConfigurationNode], remove: oldAutoLockConfigurationNode ? [oldAutoLockConfigurationNode] : [] }); + this.configurationRegistry.updateConfigurations({ add: [this.defaultBinaryEditorConfigurationNode], remove: oldDefaultBinaryEditorConfigurationNode ? [oldDefaultBinaryEditorConfigurationNode] : [] }); + this.configurationRegistry.updateConfigurations({ add: [this.editorAssociationsConfiguratioNnode], remove: oldEditorAssociationsConfigurationNode ? [oldEditorAssociationsConfigurationNode] : [] }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index d43d8c689a5..d05ff6a8ab5 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, DragAndDropObserver, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; -import { addDisposableListener, EventType, EventHelper, isAncestor } from 'vs/base/browser/dom'; +import { LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; +import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IEditorIdentifier, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; import { toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,11 +20,20 @@ import { DataTransfers } from 'vs/base/browser/dnd'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; +import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; interface IDropOperation { splitDirection?: GroupDirection; } +function isDragIntoEditorEvent(configurationService: IConfigurationService, e: DragEvent): boolean { + if (!configurationService.getValue('workbench.experimental.editor.dragAndDropIntoEditor.enabled')) { + return false; + } + return e.shiftKey; +} + class DropOverlay extends Themable { private static readonly OVERLAY_ID = 'monaco-workbench-editor-drop-overlay'; @@ -45,10 +54,12 @@ class DropOverlay extends Themable { private accessor: IEditorGroupsAccessor, private groupView: IEditorGroupView, @IThemeService themeService: IThemeService, - @IInstantiationService private instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, - @ITreeViewsService private readonly treeViewsDragAndDropService: ITreeViewsService + @ITreeViewsService private readonly treeViewsDragAndDropService: ITreeViewsService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { super(themeService); @@ -107,6 +118,11 @@ class DropOverlay extends Themable { this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { + if (isDragIntoEditorEvent(this.configurationService, e)) { + this.dispose(); + return; + } + const isDraggingGroup = this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype); const isDraggingEditor = this.editorTransfer.hasData(DraggedEditorIdentifier.prototype); @@ -311,7 +327,7 @@ class DropOverlay extends Themable { // Check for URI transfer else { - const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true /* open workspace instead of file if dropped */ }); + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: !isWeb || isTemporaryWorkspace(this.contextService.getWorkspace()) }); dropHandler.handleDrop(event, () => ensureTargetGroup(), targetGroup => targetGroup?.focus()); } } @@ -526,6 +542,7 @@ export class EditorDropTarget extends Themable { private container: HTMLElement, private readonly delegate: IEditorDropTargetDelegate, @IThemeService themeService: IThemeService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(themeService); @@ -548,6 +565,10 @@ export class EditorDropTarget extends Themable { } private onDragEnter(event: DragEvent): void { + if (isDragIntoEditorEvent(this.configurationService, event)) { + return; + } + this.counter++; // Validate transfer diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index d889b2fe531..2fdee0b8f17 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -854,8 +854,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this.model.previewEditor; } - isPinned(editor: EditorInput): boolean { - return this.model.isPinned(editor); + isPinned(editorOrIndex: EditorInput | number): boolean { + return this.model.isPinned(editorOrIndex); } isSticky(editorOrIndex: EditorInput | number): boolean { @@ -877,16 +877,27 @@ export class EditorGroupView extends Themable implements IEditorGroupView { findEditors(resource: URI, options?: IFindEditorOptions): EditorInput[] { const canonicalResource = this.uriIdentityService.asCanonicalUri(resource); return this.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => { - let matches = editor.resource && isEqual(editor.resource, canonicalResource); - - // Support side by side editor primary side if specified - if (!matches && options?.supportSideBySide === SideBySideEditor.PRIMARY) { - const primaryResource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); - - matches = primaryResource && isEqual(primaryResource, canonicalResource); + if (editor.resource && isEqual(editor.resource, canonicalResource)) { + return true; } - return matches; + // Support side by side editor primary side if specified + if (options?.supportSideBySide === SideBySideEditor.PRIMARY || options?.supportSideBySide === SideBySideEditor.ANY) { + const primaryResource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }); + if (primaryResource && isEqual(primaryResource, canonicalResource)) { + return true; + } + } + + // Support side by side editor secondary side if specified + if (options?.supportSideBySide === SideBySideEditor.SECONDARY || options?.supportSideBySide === SideBySideEditor.ANY) { + const secondaryResource = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.SECONDARY }); + if (secondaryResource && isEqual(secondaryResource, canonicalResource)) { + return true; + } + } + + return false; }); } @@ -2010,7 +2021,7 @@ export interface EditorReplacement extends IEditorReplacement { registerThemingParticipant((theme, collector) => { // Letterpress - const letterpress = `./media/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`; + const letterpress = `./media/letterpress-${theme.type}.svg`; collector.addRule(` .monaco-workbench .part.editor > .content .editor-group-container.empty .editor-group-letterpress { background-image: ${asCSSUrl(FileAccess.asBrowserUri(letterpress, require))} diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index f60356c71d7..481b9364ec6 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -10,7 +10,7 @@ import { format, compare, splitLines } from 'vs/base/common/strings'; import { extname, basename, isEqual } from 'vs/base/common/resources'; import { areFunctions, withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { Action } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IFileEditorInput, EditorResourceAccessor, IEditorPane, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; @@ -1234,14 +1234,32 @@ export class ChangeLanguageAction extends Action { // Change language if (typeof languageSelection !== 'undefined') { languageSupport.setLanguageId(languageSelection.languageId); + + if (resource?.scheme === Schemas.untitled) { + type SetUntitledDocumentLanguageEvent = { to: string; from: string }; + type SetUntitledDocumentLanguageClassification = { + to: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + owner: 'JacksonKearl'; + comment: 'Help understand effectiveness of automatic language detection'; + }; + from: { + classification: 'SystemMetaData'; + purpose: 'FeatureInsight'; + owner: 'JacksonKearl'; + comment: 'Help understand effectiveness of automatic language detection'; + }; + }; + this.telemetryService.publicLog2('setUntitledDocumentLanguage', { + to: languageSelection.languageId, + from: currentLanguageId ?? 'none', + }); + } } } activeTextEditorControl.focus(); - this.telemetryService.publicLog2('workbenchActionExecuted', { - id: ChangeLanguageAction.ID, - from: data?.from || 'quick open' - }); } } diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg b/src/vs/workbench/browser/parts/editor/media/letterpress-hcDark.svg similarity index 97% rename from src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg rename to src/vs/workbench/browser/parts/editor/media/letterpress-hcDark.svg index e91656d850c..4e32c94cf93 100644 --- a/src/vs/workbench/browser/parts/editor/media/letterpress-hc.svg +++ b/src/vs/workbench/browser/parts/editor/media/letterpress-hcDark.svg @@ -4,7 +4,7 @@ - + diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress-hcLight.svg b/src/vs/workbench/browser/parts/editor/media/letterpress-hcLight.svg new file mode 100644 index 00000000000..4edccd11a28 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/media/letterpress-hcLight.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/workbench/browser/parts/editor/media/letterpress.svg b/src/vs/workbench/browser/parts/editor/media/letterpress-light.svg similarity index 100% rename from src/vs/workbench/browser/parts/editor/media/letterpress.svg rename to src/vs/workbench/browser/parts/editor/media/letterpress-light.svg diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 9c91b6f13d9..d540048fbfd 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -289,7 +289,8 @@ flex: none; } -.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container { +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container, +.monaco-workbench.hc-light .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container { text-overflow: ellipsis; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index d2abc33ccc3..5da06c5f378 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -29,11 +29,11 @@ import { getOrSet } from 'vs/base/common/map'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BACKGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER, TAB_HOVER_BACKGROUND, TAB_HOVER_BORDER, TAB_UNFOCUSED_HOVER_BACKGROUND, TAB_UNFOCUSED_HOVER_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, WORKBENCH_BACKGROUND, TAB_ACTIVE_BORDER_TOP, TAB_UNFOCUSED_ACTIVE_BORDER_TOP, TAB_ACTIVE_MODIFIED_BORDER, TAB_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER, TAB_UNFOCUSED_INACTIVE_BACKGROUND, TAB_HOVER_FOREGROUND, TAB_UNFOCUSED_HOVER_FOREGROUND, EDITOR_GROUP_HEADER_TABS_BORDER, TAB_LAST_PINNED_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder, editorBackground, breadcrumbsBackground } from 'vs/platform/theme/common/colorRegistry'; -import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DragAndDropObserver, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdentifier, DraggedTreeItemsIdentifier, extractTreeDropData } from 'vs/workbench/browser/dnd'; import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, EventHelper, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode, DragAndDropObserver } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction, UnpinEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; @@ -47,7 +47,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPath, win32, posix } from 'vs/base/common/path'; import { coalesce, insert } from 'vs/base/common/arrays'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { isSafari } from 'vs/base/browser/browser'; import { equals } from 'vs/base/common/objects'; import { EditorActivation } from 'vs/platform/editor/common/editor'; @@ -1857,7 +1857,7 @@ export class TabsTitleControl extends TitleControl { // Check for URI transfer else { - const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false /* open workspace file as file if dropped */ }); + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: false }); dropHandler.handleDrop(e, () => this.group, () => this.group.focus(), targetIndex); } } @@ -1882,7 +1882,7 @@ export class TabsTitleControl extends TitleControl { registerThemingParticipant((theme, collector) => { // Add border between tabs and breadcrumbs in high contrast mode. - if (theme.type === ColorScheme.HIGH_CONTRAST) { + if (isHighContrast(theme.type)) { const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); if (borderColor) { collector.addRule(` @@ -2020,7 +2020,7 @@ registerThemingParticipant((theme, collector) => { // - in high contrast theme // - if we have a contrast border (which draws an outline - https://github.com/microsoft/vscode/issues/109117) // - on Safari (https://github.com/microsoft/vscode/issues/108996) - if (theme.type !== 'hc' && !isSafari && !activeContrastBorderColor) { + if (isHighContrast(theme.type) && !isSafari && !activeContrastBorderColor) { const workbenchBackground = WORKBENCH_BACKGROUND(theme); const editorBackgroundColor = theme.getColor(editorBackground); const editorGroupHeaderTabsBackground = theme.getColor(EDITOR_GROUP_HEADER_TABS_BACKGROUND); diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index f7d4fadab04..1822c2661df 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -168,7 +168,7 @@ export class TextResourceEditor extends AbstractTextResourceEditor { } if (e.range.startLineNumber !== 1 || e.range.startColumn !== 1) { - return; // only when pasting into first line, first column (= empty document) + return; // document had existing content before the pasted text, don't override. } if (codeEditor.getOption(EditorOption.readOnly)) { @@ -180,29 +180,42 @@ export class TextResourceEditor extends AbstractTextResourceEditor { return; // require a live model } + const pasteIsWholeContents = textModel.getLineCount() === e.range.endLineNumber && textModel.getLineMaxColumn(e.range.endLineNumber) === e.range.endColumn; + if (!pasteIsWholeContents) { + return; // document had existing content after the pasted text, don't override. + } + const currentLanguageId = textModel.getLanguageId(); if (currentLanguageId !== PLAINTEXT_LANGUAGE_ID) { return; // require current languageId to be unspecific } - let candidateLanguageId: string | undefined = undefined; + let candidateLanguage: { id: string; source: 'event' | 'guess' } | undefined = undefined; // A languageId is provided via the paste event so text was copied using // VSCode. As such we trust this languageId and use it if specific if (e.languageId) { - candidateLanguageId = e.languageId; + candidateLanguage = { id: e.languageId, source: 'event' }; } // A languageId was not provided, so the data comes from outside VSCode // We can still try to guess a good languageId from the first line if // the paste changed the first line else { - candidateLanguageId = withNullAsUndefined(this.languageService.guessLanguageIdByFilepathOrFirstLine(textModel.uri, textModel.getLineContent(1).substr(0, ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT))); + const guess = withNullAsUndefined(this.languageService.guessLanguageIdByFilepathOrFirstLine(textModel.uri, textModel.getLineContent(1).substr(0, ModelConstants.FIRST_LINE_DETECTION_LENGTH_LIMIT))); + if (guess) { + candidateLanguage = { id: guess, source: 'guess' }; + } } // Finally apply languageId to model if specified - if (candidateLanguageId !== PLAINTEXT_LANGUAGE_ID) { - this.modelService.setMode(textModel, this.languageService.createById(candidateLanguageId)); + if (candidateLanguage && candidateLanguage.id !== PLAINTEXT_LANGUAGE_ID) { + if (this.input instanceof UntitledTextEditorInput && candidateLanguage.source === 'event') { + // High confidence, set language id at TextEditorModel level to block future auto-detection + this.input.model.setLanguageId(candidateLanguage.id); + } else { + this.modelService.setMode(textModel, this.languageService.createById(candidateLanguage.id)); + } } } } diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css index 987f9a3686d..58b74fb0785 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsToasts.css @@ -32,6 +32,10 @@ transition: transform 300ms ease-out, opacity 300ms ease-out; } +.monaco-workbench.reduce-motion > .notifications-toasts .notification-toast-container > .notification-toast { + transition: transform 0ms ease-out, opacity 0ms ease-out; +} + .monaco-workbench > .notifications-toasts .notification-toast-container > .notification-toast.notification-fade-in { opacity: 1; transform: none; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index 7922f858004..3b0d34ea4af 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -154,10 +154,10 @@ interface NotificationActionMetrics { } type NotificationActionMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the action that was run from a notification.' }; + actionLabel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The label of the action that was run from a notification.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification where an action was run.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whether the notification where an action was run is silent or not.' }; }; export class NotificationActionRunner extends ActionRunner { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index 17d5d53195f..b889b26f88a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts @@ -45,31 +45,31 @@ export class NotificationsAlerts extends Disposable { } } - private triggerAriaAlert(notifiation: INotificationViewItem): void { - if (notifiation.silent) { + private triggerAriaAlert(notification: INotificationViewItem): void { + if (notification.silent) { return; } // Trigger the alert again whenever the message changes - const listener = notifiation.onDidChangeContent(e => { + const listener = notification.onDidChangeContent(e => { if (e.kind === NotificationViewItemContentChangeKind.MESSAGE) { - this.doTriggerAriaAlert(notifiation); + this.doTriggerAriaAlert(notification); } }); - Event.once(notifiation.onDidClose)(() => listener.dispose()); + Event.once(notification.onDidClose)(() => listener.dispose()); - this.doTriggerAriaAlert(notifiation); + this.doTriggerAriaAlert(notification); } - private doTriggerAriaAlert(notifiation: INotificationViewItem): void { + private doTriggerAriaAlert(notification: INotificationViewItem): void { let alertText: string; - if (notifiation.severity === Severity.Error) { - alertText = localize('alertErrorMessage', "Error: {0}", notifiation.message.linkedText.toString()); - } else if (notifiation.severity === Severity.Warning) { - alertText = localize('alertWarningMessage', "Warning: {0}", notifiation.message.linkedText.toString()); + if (notification.severity === Severity.Error) { + alertText = localize('alertErrorMessage', "Error: {0}", notification.message.linkedText.toString()); + } else if (notification.severity === Severity.Warning) { + alertText = localize('alertWarningMessage', "Warning: {0}", notification.message.linkedText.toString()); } else { - alertText = localize('alertInfoMessage', "Info: {0}", notifiation.message.linkedText.toString()); + alertText = localize('alertInfoMessage', "Info: {0}", notification.message.linkedText.toString()); } alert(alertText); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts index 33947641bbd..b498207420e 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsTelemetry.ts @@ -16,9 +16,9 @@ export interface NotificationMetrics { } export type NotificationMetricsClassification = { - id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; + id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The identifier of the source of the notification.' }; + silent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'Whethe the notification is silent or not.' }; + source?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'bpasero'; comment: 'The source of the notification.' }; }; export function notificationToMetrics(message: NotificationMessage, source: string | undefined, silent: boolean): NotificationMetrics { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index fc96d2263ef..355de6cf73a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -8,7 +8,7 @@ import { clearNode, addDisposableListener, EventType, EventHelper, $, EventLike import { IOpenerService } from 'vs/platform/opener/common/opener'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { ButtonBar, IButtonOptions } from 'vs/base/browser/ui/button/button'; import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -453,8 +453,10 @@ export class NotificationTemplateRenderer extends Disposable { const primaryActions = notification.actions ? notification.actions.primary : undefined; if (notification.expanded && isNonEmptyArray(primaryActions)) { const that = this; + const actionRunner: IActionRunner = new class extends ActionRunner { protected override async runAction(action: IAction): Promise { + // Run action that.actionRunner.run(action, notification); @@ -464,24 +466,34 @@ export class NotificationTemplateRenderer extends Disposable { } } }(); + const buttonToolbar = this.inputDisposables.add(new ButtonBar(this.template.buttonsContainer)); - for (const action of primaryActions) { - const buttonOptions = { title: true, /* assign titles to buttons in case they overflow */ }; + for (let i = 0; i < primaryActions.length; i++) { + const action = primaryActions[i]; + + const options: IButtonOptions = { + title: true, // assign titles to buttons in case they overflow + secondary: i > 0 + }; + const dropdownActions = action instanceof ChoiceAction ? action.menu : undefined; - const button = this.inputDisposables.add( - dropdownActions - ? buttonToolbar.addButtonWithDropdown({ - ...buttonOptions, - contextMenuProvider: this.contextMenuService, - actions: dropdownActions, - actionRunner - }) - : buttonToolbar.addButton(buttonOptions)); + const button = this.inputDisposables.add(dropdownActions ? + buttonToolbar.addButtonWithDropdown({ + ...options, + contextMenuProvider: this.contextMenuService, + actions: dropdownActions, + actionRunner + }) : + buttonToolbar.addButton(options) + ); + button.label = action.label; + this.inputDisposables.add(button.onDidClick(e => { if (e) { EventHelper.stop(e, true); } + actionRunner.run(action); })); diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 22142adcb3a..54685c13787 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -396,7 +396,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'workbench.action.closeAuxiliaryBar', - title: { value: localize('closeSidePanel', "Close Side Panel"), original: 'Close Side Panel' }, + title: { value: localize('closeSecondarySideBar', "Close Secondary Side Bar"), original: 'Close Secondary Side Bar' }, category: CATEGORIES.View, icon: closeIcon, menu: [{ @@ -490,42 +490,68 @@ class MoveViewsBetweenPanelsAction extends Action2 { } } -// --- Move Panel Views To Side Panel +// --- Move Panel Views To Secondary Side Bar -export class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction { +class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction { static readonly ID = 'workbench.action.movePanelToSidePanel'; constructor() { super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, { id: MovePanelToSidePanelAction.ID, title: { - value: localize('movePanelToSidePanel', "Move Panel Views To Side Panel"), - original: 'Move Panel Views To Side Panel' + value: localize('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"), + original: 'Move Panel Views To Secondary Side Bar' }, category: CATEGORIES.View, - f1: true, - menu: [{ - id: MenuId.ViewContainerTitleContext, - group: '3_workbench_layout_move', - order: 0, - when: ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Panel)), - }] + f1: true + }); + } +} + +export class MovePanelToSecondarySideBarAction extends MoveViewsBetweenPanelsAction { + static readonly ID = 'workbench.action.movePanelToSecondarySideBar'; + constructor() { + super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, { + id: MovePanelToSecondarySideBarAction.ID, + title: { + value: localize('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"), + original: 'Move Panel Views To Secondary Side Bar' + }, + category: CATEGORIES.View, + f1: true }); } } registerAction2(MovePanelToSidePanelAction); +registerAction2(MovePanelToSecondarySideBarAction); -// --- Move Panel Views To Side Panel +// --- Move Secondary Side Bar Views To Panel -export class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction { +class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction { static readonly ID = 'workbench.action.moveSidePanelToPanel'; constructor() { super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, { id: MoveSidePanelToPanelAction.ID, title: { - value: localize('moveSidePanelToPanel', "Move Side Panel Views To Panel"), - original: 'Move Side Panel Views To Panel' + value: localize('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"), + original: 'Move Secondary Side Bar Views To Panel' + }, + category: CATEGORIES.View, + f1: true + }); + } +} + +export class MoveSecondarySideBarToPanelAction extends MoveViewsBetweenPanelsAction { + static readonly ID = 'workbench.action.moveSecondarySideBarToPanel'; + + constructor() { + super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, { + id: MoveSecondarySideBarToPanelAction.ID, + title: { + value: localize('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"), + original: 'Move Secondary Side Bar Views To Panel' }, category: CATEGORIES.View, f1: true @@ -533,3 +559,4 @@ export class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction { } } registerAction2(MoveSidePanelToPanelAction); +registerAction2(MoveSecondarySideBarToPanelAction); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e671cd4b144..3e87cd1f5fd 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -18,7 +18,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction, MovePanelToSidePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; +import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -493,7 +493,7 @@ export abstract class BasePanelPart extends CompositePart impleme const messageElement = document.createElement('div'); messageElement.classList.add('empty-panel-message'); - messageElement.innerText = localize('panel.emptyMessage', "Drag a view into the panel to display."); + messageElement.innerText = localize('panel.emptyMessage', "Drag a view here to display."); this.emptyPanelMessageElement.appendChild(messageElement); contentArea.appendChild(this.emptyPanelMessageElement); @@ -965,7 +965,6 @@ export class PanelPart extends BasePanelPart { actions.push(...[ new Separator(), - toAction({ id: MovePanelToSidePanelAction.ID, label: localize('moveToSidePanel', "Move Views to Side Panel"), run: () => this.instantiationService.invokeFunction(accessor => new MovePanelToSidePanelAction().run(accessor)) }), ...PositionPanelActionConfigs // show the contextual menu item if it is not in that position .filter(({ when }) => this.contextKeyService.contextMatchesRules(when)) diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts index e2d436c64f2..f1ab76ec17b 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarActions.ts @@ -19,7 +19,7 @@ export class FocusSideBarAction extends Action2 { constructor() { super({ id: 'workbench.action.focusSideBar', - title: { value: localize('focusSideBar', "Focus into Side Bar"), original: 'Focus into Side Bar' }, + title: { value: localize('focusSideBar', "Focus into Primary Side Bar"), original: 'Focus into Primary Side Bar' }, category: CATEGORIES.View, f1: true, keybinding: { diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index fe96335dc36..8a387c00f71 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -14,10 +14,6 @@ transition: background-color 0.35s ease-out; } -.monaco-workbench .part.statusbar:focus { - outline-color: var(--statusbar-focusborder); -} - .monaco-workbench .part.statusbar.status-border-top::after { content: ''; position: absolute; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index dd9e1341a53..ecee307637e 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -13,7 +13,7 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntry import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IAction, Separator, toAction } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER } from 'vs/workbench/common/theme'; +import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER, STATUS_BAR_FOCUS_BORDER } from 'vs/workbench/common/theme'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { EventHelper, createStyleSheet, addDisposableListener, EventType, clearNode } from 'vs/base/browser/dom'; @@ -25,7 +25,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; import { assertIsDefined } from 'vs/base/common/types'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { hash } from 'vs/base/common/hash'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -88,7 +88,10 @@ export class StatusbarPart extends Part implements IStatusbarService { ) { } showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined { - return this.hoverService.showHover(options, focus); + return this.hoverService.showHover({ + ...options, + hideOnKeyDown: true + }, focus); } onDidHideHover(): void { @@ -418,7 +421,7 @@ export class StatusbarPart extends Part implements IStatusbarService { const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND)?.toString(); const statusBarItemCompactHoverBackground = this.getColor(STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND)?.toString(); this.compactEntriesDisposable.value = new DisposableStore(); - if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && this.theme.type !== ColorScheme.HIGH_CONTRAST) { + if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) { for (const [, compactEntryGroup] of compactEntryGroups) { for (const compactEntry of compactEntryGroup) { if (!compactEntry.hasCommand) { @@ -521,12 +524,19 @@ export class StatusbarPart extends Part implements IStatusbarService { // Colors and focus outlines via dynamic stylesheet + const statusBarFocusColor = this.getColor(STATUS_BAR_FOCUS_BORDER); + if (!this.styleElement) { this.styleElement = createStyleSheet(container); } this.styleElement.textContent = ` - /* Focus outline */ + /* Status bar focus outline */ + .monaco-workbench .part.statusbar:focus { + outline-color: ${statusBarFocusColor}; + } + + /* Status bar item focus outline */ .monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus-visible:not(.disabled) { outline: 1px solid ${this.getColor(activeContrastBorder) ?? itemBorderColor}; outline-offset: ${borderColor ? '-2px' : '-1px'}; @@ -561,7 +571,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } registerThemingParticipant((theme, collector) => { - if (theme.type !== ColorScheme.HIGH_CONTRAST) { + if (!isHighContrast(theme.type)) { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { collector.addRule(`.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) { background-color: ${statusBarItemHoverBackground}; }`); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index c56ce5b3e24..2471bcee7f8 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -191,7 +191,7 @@ background-color: rgba(255, 255, 255, 0.1); } -.monaco-workbench .part.titlebar > .titlebar-container.light > .window-controls-container > .window-icon:hover { +.monaco-workbench .part.titlebar.light > .titlebar-container > .window-controls-container > .window-icon:hover { background-color: rgba(0, 0, 0, 0.1); } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index c65279d176f..2a65c919982 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -293,7 +293,7 @@ export class TitlebarPart extends Part implements ITitleService { // vscode-remtoe: use as is // otherwise figure out if we have a virtual folder opened let remoteName: string | undefined = undefined; - if (this.environmentService.remoteAuthority) { + if (this.environmentService.remoteAuthority && !isWeb) { remoteName = this.labelService.getHostLabel(Schemas.vscodeRemote, this.environmentService.remoteAuthority); } else { const virtualWorkspaceLocation = getVirtualWorkspaceLocation(workspace); @@ -594,7 +594,7 @@ registerThemingParticipant((theme, collector) => { const titlebarActiveFg = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND); if (titlebarActiveFg) { collector.addRule(` - .monaco-workbench .part.titlebar > .window-controls-container .window-icon { + .monaco-workbench .part.titlebar .window-controls-container .window-icon { color: ${titlebarActiveFg}; } `); @@ -603,7 +603,7 @@ registerThemingParticipant((theme, collector) => { const titlebarInactiveFg = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND); if (titlebarInactiveFg) { collector.addRule(` - .monaco-workbench .part.titlebar.inactive > .window-controls-container .window-icon { + .monaco-workbench .part.titlebar.inactive .window-controls-container .window-icon { color: ${titlebarInactiveFg}; } `); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9b412e5c503..1d556b829bd 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, registerAction2, Action2, IMenu } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer } from 'vs/workbench/common/views'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -63,6 +63,7 @@ import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViews import { generateUuid } from 'vs/base/common/uuid'; import { ILogService } from 'vs/platform/log/common/log'; import { Mimes } from 'vs/base/common/mime'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; export class TreeViewPane extends ViewPane { @@ -1378,7 +1379,7 @@ export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { if (!originalEvent.dataTransfer || !dndController || !targetNode) { return; } - const treeDataTransfer: ITreeDataTransfer = new Map(); + const treeDataTransfer: IDataTransfer = new Map(); let stringCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { if (current.kind === 'string') { return previous + 1; diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index fa70a4f92f9..80be8b96724 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -25,7 +25,7 @@ import { Extensions as ViewContainerExtensions, IView, IViewDescriptorService, V import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { MenuId, Action2, IAction2Options, IMenuService } from 'vs/platform/actions/common/actions'; +import { MenuId, Action2, IAction2Options, IMenuService, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { parseLinkedText } from 'vs/base/common/linkedText'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -41,6 +41,7 @@ import { URI } from 'vs/base/common/uri'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; import { CompositeMenuActions } from 'vs/workbench/browser/actions'; +import { IDropdownMenuActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; export interface IViewPaneOptions extends IPaneOptions { id: string; @@ -508,8 +509,8 @@ export abstract class ViewPane extends Pane implements IView { this._onDidChangeTitleArea.fire(); } - getActionViewItem(action: IAction): IActionViewItem | undefined { - return createActionViewItem(this.instantiationService, action); + getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined { + return createActionViewItem(this.instantiationService, action, { ...options, ...{ menuAsChild: action instanceof SubmenuItemAction } }); } getActionsContext(): unknown { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 59872456427..be75bf31cd1 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener, Dimension, EventType, isAncestor } from 'vs/base/browser/dom'; +import { addDisposableListener, Dimension, DragAndDropObserver, EventType, isAncestor } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -31,7 +31,7 @@ import { attachStyler, IColorMapping } from 'vs/platform/theme/common/styler'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { CompositeMenuActions } from 'vs/workbench/browser/actions'; -import { CompositeDragAndDropObserver, DragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; +import { CompositeDragAndDropObserver, toggleDropEffect } from 'vs/workbench/browser/dnd'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { Component } from 'vs/workbench/common/component'; @@ -692,9 +692,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private saveViewSizes(): void { // Save size only when the layout has happened if (this.didLayout) { - for (const view of this.panes) { - this.viewContainerModel.setSize(view.id, this.getPaneSize(view)); - } + this.viewContainerModel.setSizes(this.panes.map(view => ({ id: view.id, size: this.getPaneSize(view) }))); } } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 395842cd56f..31d0aa03440 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -10,7 +10,7 @@ import { WORKBENCH_BACKGROUND, TITLE_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/ import { isWeb, isIOS, isMacintosh, isWindows } from 'vs/base/common/platform'; import { createMetaElement } from 'vs/base/browser/dom'; import { isSafari, isStandalone } from 'vs/base/browser/browser'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; registerThemingParticipant((theme, collector) => { @@ -92,7 +92,7 @@ registerThemingParticipant((theme, collector) => { } // High Contrast theme overwrites for outline - if (theme.type === ColorScheme.HIGH_CONTRAST) { + if (isHighContrast(theme.type)) { collector.addRule(` .hc-black [tabindex="0"]:focus, .hc-black [tabindex="-1"]:focus, @@ -101,12 +101,21 @@ registerThemingParticipant((theme, collector) => { .hc-black input[type="button"]:focus, .hc-black input[type="text"]:focus, .hc-black textarea:focus, - .hc-black input[type="checkbox"]:focus { + .hc-black input[type="checkbox"]:focus, + .hc-light [tabindex="0"]:focus, + .hc-light [tabindex="-1"]:focus, + .hc-light .synthetic-focus, + .hc-light select:focus, + .hc-light input[type="button"]:focus, + .hc-light input[type="text"]:focus, + .hc-light textarea:focus, + .hc-light input[type="checkbox"]:focus { outline-style: solid; outline-width: 1px; } - .hc-black .synthetic-focus input { + .hc-black .synthetic-focus input {, + .hc-light .synthetic-focus input background: transparent; /* Search input focus fix when in high contrast */ } `); diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 44593e92807..ddceb3aeeb8 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -170,6 +170,11 @@ export interface IWorkbenchConstructionOptions { */ readonly additionalTrustedDomains?: string[]; + /** + * Enable workspace trust feature for the current window + */ + readonly enableWorkspaceTrust?: boolean; + /** * Urls that will be opened externally that are allowed access * to the opener window. This is primarily used to allow @@ -512,7 +517,8 @@ export interface IWindowIndicator { export enum ColorScheme { DARK = 'dark', LIGHT = 'light', - HIGH_CONTRAST = 'hc' + HIGH_CONTRAST_LIGHT = 'hcLight', + HIGH_CONTRAST_DARK = 'hcDark' } export interface IInitialColorTheme { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 5cfebbd665b..260d8438a27 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -330,7 +330,7 @@ export class BrowserMain extends Disposable { logService.info('Using in-memory user data provider'); userDataProvider = new InMemoryFileSystemProvider(); } - fileService.registerProvider(Schemas.userData, userDataProvider); + fileService.registerProvider(Schemas.vscodeUserData, userDataProvider); // Remote file system this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); @@ -406,7 +406,7 @@ export class BrowserMain extends Disposable { } private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: IWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { - const configurationCache = new ConfigurationCache([Schemas.file, Schemas.userData, Schemas.tmp] /* Cache all non native resources */, environmentService, fileService); + const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData, Schemas.tmp] /* Cache all non native resources */, environmentService, fileService); const workspaceService = new WorkspaceService({ remoteAuthority: this.configuration.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); try { diff --git a/src/vs/workbench/browser/webview.ts b/src/vs/workbench/browser/webview.ts new file mode 100644 index 00000000000..27ad38106b7 --- /dev/null +++ b/src/vs/workbench/browser/webview.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * Returns a sha-256 composed of `parentOrigin` and `salt` converted to base 32 + */ +export async function parentOriginHash(parentOrigin: string, salt: string): Promise { + // This same code is also inlined at `src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html` + if (!crypto.subtle) { + throw new Error(`Can't compute sha-256`); + } + const strData = JSON.stringify({ parentOrigin, salt }); + const encoder = new TextEncoder(); + const arrData = encoder.encode(strData); + const hash = await crypto.subtle.digest('sha-256', arrData); + return sha256AsBase32(hash); +} + +function sha256AsBase32(bytes: ArrayBuffer): string { + const array = Array.from(new Uint8Array(bytes)); + const hexArray = array.map(b => b.toString(16).padStart(2, '0')).join(''); + // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 + return BigInt(`0x${hexArray}`).toString(32).padStart(52, '0'); +} diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index d4a40dba3df..cbcbac8602c 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -195,7 +195,7 @@ export class BrowserWindow extends Disposable { private registerLabelFormatters() { this._register(this.labelService.registerFormatter({ - scheme: Schemas.userData, + scheme: Schemas.vscodeUserData, priority: true, formatting: { label: '(Settings) ${path}', diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 6eb31701e2b..059ffcba78d 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -101,7 +101,8 @@ const registry = Registry.as(ConfigurationExtensions.Con }, 'workbench.editor.historyBasedLanguageDetection': { type: 'boolean', - default: false, + default: true, + tags: ['experimental'], description: localize('workbench.editor.historyBasedLanguageDetection', "Enables use of editor history in language detection. This causes automatic language detection to favor languages that have been recently opened and allows for automatic language detection to operate with smaller inputs."), }, 'workbench.editor.tabCloseButton': { @@ -256,10 +257,43 @@ const registry = Registry.as(ConfigurationExtensions.Con 'default': false, 'description': localize('perEditorGroup', "Controls if the limit of maximum opened editors should apply per editor group or across all editor groups.") }, + 'workbench.localHistory.enabled': { + 'type': 'boolean', + 'default': true, + 'description': localize('localHistoryEnabled', "Controls whether local file history is enabled. When enabled, the file contents of an editor that is saved will be stored to a backup location to be able to restore or review the contents later. Changing this setting has no effect on existing local file history entries."), + 'scope': ConfigurationScope.RESOURCE + }, + 'workbench.localHistory.maxFileSize': { + 'type': 'number', + 'default': 256, + 'minimum': 1, + 'description': localize('localHistoryMaxFileSize', "Controls the maximum size of a file (in KB) to be considered for local file history. Files that are larger will not be added to the local file history. Changing this setting has no effect on existing local file history entries."), + 'scope': ConfigurationScope.RESOURCE + }, + 'workbench.localHistory.maxFileEntries': { + 'type': 'number', + 'default': 50, + 'minimum': 0, + 'description': localize('localHistoryMaxFileEntries', "Controls the maximum number of local file history entries per file. When the number of local file history entries exceeds this number for a file, the oldest entries will be discarded."), + 'scope': ConfigurationScope.RESOURCE + }, + 'workbench.localHistory.exclude': { + 'type': 'object', + 'markdownDescription': localize('exclude', "Configure [glob patterns](https://code.visualstudio.com/docs/editor/codebasics#_advanced-search-options) for excluding files from the local file history. Changing this setting has no effect on existing local file history entries."), + 'scope': ConfigurationScope.RESOURCE + }, + 'workbench.localHistory.mergePeriod': { + 'type': 'number', + 'default': 10, + 'minimum': 1, + 'markdownDescription': localize('mergePeriod', "Configure an interval in seconds during which the last entry in local file history is replaced with the entry that is being added. This helps reduce the overall number of entries that are added, for example when auto save is enabled. This setting is only applied to entries that have the same source of origin. Changing this setting has no effect on existing local file history entries."), + 'scope': ConfigurationScope.RESOURCE + }, 'workbench.commandPalette.history': { 'type': 'number', 'description': localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."), - 'default': 50 + 'default': 50, + 'minimum': 0 }, 'workbench.commandPalette.preserveInput': { 'type': 'boolean', @@ -295,7 +329,7 @@ const registry = Registry.as(ConfigurationExtensions.Con 'type': 'string', 'enum': ['left', 'right'], 'default': 'left', - 'description': localize('sideBarLocation', "Controls the location of the sidebar and activity bar. They can either show on the left or right of the workbench.") + 'description': localize('sideBarLocation', "Controls the location of the primary sidebar and activity bar. They can either show on the left or right of the workbench. The secondary side bar will show on the opposite side of the workbench.") }, 'workbench.panel.defaultLocation': { 'type': 'string', @@ -369,7 +403,19 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."), // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. // On Mac, the delay is 1500. - 'default': isMacintosh ? 1500 : 500 + 'default': isMacintosh ? 1500 : 500, + 'minimum': 0 + }, + 'workbench.reduceMotion': { + type: 'string', + description: localize('workbench.reduceMotion', "Controls whether the workbench should render with fewer animations."), + 'enumDescriptions': [ + localize('workbench.reduceMotion.on', "Always render with reduced motion."), + localize('workbench.reduceMotion.off', "Do not render with reduced motion"), + localize('workbench.reduceMotion.auto', "Render with reduced motion based on OS configuration."), + ], + default: 'auto', + enum: ['on', 'off', 'auto'] }, 'workbench.experimental.layoutControl.enabled': { 'type': 'boolean', @@ -386,9 +432,15 @@ const registry = Registry.as(ConfigurationExtensions.Con localize('layoutcontrol.type.both', "Shows both the dropdown and toggle buttons."), ], 'tags': ['experimental'], - 'default': 'menu', + 'default': 'both', 'description': localize('layoutControlType', "Controls whether the layout control in the custom title bar is displayed as a single menu button or with multiple UI toggles."), }, + 'workbench.experimental.editor.dragAndDropIntoEditor.enabled': { + 'type': 'boolean', + 'tags': ['experimental'], + 'default': false, + 'description': localize('dragAndDropIntoEditor', "Controls whether you can drag and drop a file into an editor by holding down shift (instead of opening the file in an editor)."), + } } }); diff --git a/src/vs/workbench/buildfile.desktop.js b/src/vs/workbench/buildfile.desktop.js deleted file mode 100644 index 5f40e47276b..00000000000 --- a/src/vs/workbench/buildfile.desktop.js +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -const { createModuleDescription, createEditorWorkerModuleDescription } = require('../base/buildfile'); - -exports.collectModules = function () { - return [ - createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), - - createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), - - createModuleDescription('vs/platform/files/node/watcher/watcherMain'), - - createModuleDescription('vs/platform/terminal/node/ptyHostMain'), - - createModuleDescription('vs/workbench/api/node/extensionHostProcess'), - ]; -}; diff --git a/src/vs/workbench/buildfile.web.js b/src/vs/workbench/buildfile.web.js deleted file mode 100644 index 3770c48ecc0..00000000000 --- a/src/vs/workbench/buildfile.web.js +++ /dev/null @@ -1,14 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -const { createModuleDescription, createEditorWorkerModuleDescription } = require('../base/buildfile'); - -exports.collectModules = function () { - return [ - createEditorWorkerModuleDescription('vs/workbench/contrib/output/common/outputLinkComputer'), - createModuleDescription('vs/code/browser/workbench/workbench', ['vs/workbench/workbench.web.main']), - ]; -}; diff --git a/extensions/sql/build/update-grammar.js b/src/vs/workbench/common/dnd.ts similarity index 64% rename from extensions/sql/build/update-grammar.js rename to src/vs/workbench/common/dnd.ts index aecb5aa2704..4d92fb48f9c 100644 --- a/extensions/sql/build/update-grammar.js +++ b/src/vs/workbench/common/dnd.ts @@ -2,9 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; - -var updateGrammar = require('vscode-grammar-updater'); -updateGrammar.update('microsoft/vscode-mssql', 'syntaxes/SQL.plist', './syntaxes/sql.tmLanguage.json', undefined, 'main'); +export interface IDataTransferItem { + asString(): Thenable; + value: any; +} +export type IDataTransfer = Map; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 5434a0e5da3..235d5d71f34 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -549,6 +549,38 @@ export const enum SaveReason { WINDOW_CHANGE = 4 } +export type SaveSource = string; + +interface ISaveSourceDescriptor { + source: SaveSource; + label: string; +} + +class SaveSourceFactory { + + private readonly mapIdToSaveSource = new Map(); + + /** + * Registers a `SaveSource` with an identifier and label + * to the registry so that it can be used in save operations. + */ + registerSource(id: string, label: string): SaveSource { + let sourceDescriptor = this.mapIdToSaveSource.get(id); + if (!sourceDescriptor) { + sourceDescriptor = { source: id, label }; + this.mapIdToSaveSource.set(id, sourceDescriptor); + } + + return sourceDescriptor.source; + } + + getSourceLabel(source: SaveSource): string { + return this.mapIdToSaveSource.get(source)?.label ?? source; + } +} + +export const SaveSourceRegistry = new SaveSourceFactory(); + export interface ISaveOptions { /** @@ -556,6 +588,13 @@ export interface ISaveOptions { */ reason?: SaveReason; + /** + * An indicator about the source of the save operation. + * + * Must use `SaveSourceRegistry.registerSource()` to obtain. + */ + readonly source?: SaveSource; + /** * Forces to save the contents of the working copy * again even if the working copy is not dirty. @@ -996,11 +1035,11 @@ export enum SideBySideEditor { export interface IFindEditorOptions { /** - * Whether to consider a side by side primary editor as matching. + * Whether to consider any or both side by side editor as matching. * By default, side by side editors will not be considered * as matching, even if the editor is opened in one of the sides. */ - supportSideBySide?: SideBySideEditor.PRIMARY; + supportSideBySide?: SideBySideEditor.PRIMARY | SideBySideEditor.SECONDARY | SideBySideEditor.ANY; } export interface IMatchEditorOptions { diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 20f8a5d6a31..5707215da91 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -4,20 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, editorBackground, contrastBorder, transparent, editorWidgetBackground, textLinkForeground, lighten, darken, focusBorder, activeContrastBorder, editorWidgetForeground, editorErrorForeground, editorWarningForeground, editorInfoForeground, treeIndentGuidesStroke, errorForeground, listActiveSelectionBackground, listActiveSelectionForeground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; // < --- Workbench (not customizable) --- > export function WORKBENCH_BACKGROUND(theme: IColorTheme): Color { switch (theme.type) { - case 'dark': - return Color.fromHex('#252526'); - case 'light': + case ColorScheme.LIGHT: return Color.fromHex('#F3F3F3'); - default: + case ColorScheme.HIGH_CONTRAST_LIGHT: + return Color.fromHex('#FFFFFF'); + case ColorScheme.HIGH_CONTRAST_DARK: return Color.fromHex('#000000'); + default: + return Color.fromHex('#252526'); } } @@ -28,25 +31,29 @@ export function WORKBENCH_BACKGROUND(theme: IColorTheme): Color { export const TAB_ACTIVE_BACKGROUND = registerColor('tab.activeBackground', { dark: editorBackground, light: editorBackground, - hc: editorBackground + hcDark: editorBackground, + hcLight: editorBackground }, localize('tabActiveBackground', "Active tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BACKGROUND = registerColor('tab.unfocusedActiveBackground', { dark: TAB_ACTIVE_BACKGROUND, light: TAB_ACTIVE_BACKGROUND, - hc: TAB_ACTIVE_BACKGROUND + hcDark: TAB_ACTIVE_BACKGROUND, + hcLight: TAB_ACTIVE_BACKGROUND, }, localize('tabUnfocusedActiveBackground', "Active tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_BACKGROUND = registerColor('tab.inactiveBackground', { dark: '#2D2D2D', light: '#ECECEC', - hc: null + hcDark: null, + hcLight: null, }, localize('tabInactiveBackground', "Inactive tab background color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedInactiveBackground', { dark: TAB_INACTIVE_BACKGROUND, light: TAB_INACTIVE_BACKGROUND, - hc: TAB_INACTIVE_BACKGROUND + hcDark: TAB_INACTIVE_BACKGROUND, + hcLight: TAB_INACTIVE_BACKGROUND }, localize('tabUnfocusedInactiveBackground', "Inactive tab background color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -56,25 +63,29 @@ export const TAB_UNFOCUSED_INACTIVE_BACKGROUND = registerColor('tab.unfocusedIna export const TAB_ACTIVE_FOREGROUND = registerColor('tab.activeForeground', { dark: Color.white, light: '#333333', - hc: Color.white + hcDark: Color.white, + hcLight: '#292929' }, localize('tabActiveForeground', "Active tab foreground color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_FOREGROUND = registerColor('tab.inactiveForeground', { dark: transparent(TAB_ACTIVE_FOREGROUND, 0.5), light: transparent(TAB_ACTIVE_FOREGROUND, 0.7), - hc: Color.white + hcDark: Color.white, + hcLight: '#292929' }, localize('tabInactiveForeground', "Inactive tab foreground color in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_FOREGROUND = registerColor('tab.unfocusedActiveForeground', { dark: transparent(TAB_ACTIVE_FOREGROUND, 0.5), light: transparent(TAB_ACTIVE_FOREGROUND, 0.7), - hc: Color.white + hcDark: Color.white, + hcLight: '#292929' }, localize('tabUnfocusedActiveForeground', "Active tab foreground color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedInactiveForeground', { dark: transparent(TAB_INACTIVE_FOREGROUND, 0.5), light: transparent(TAB_INACTIVE_FOREGROUND, 0.5), - hc: Color.white + hcDark: Color.white, + hcLight: '#292929' }, localize('tabUnfocusedInactiveForeground', "Inactive tab foreground color in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -84,25 +95,29 @@ export const TAB_UNFOCUSED_INACTIVE_FOREGROUND = registerColor('tab.unfocusedIna export const TAB_HOVER_BACKGROUND = registerColor('tab.hoverBackground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('tabHoverBackground', "Tab background color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BACKGROUND = registerColor('tab.unfocusedHoverBackground', { dark: transparent(TAB_HOVER_BACKGROUND, 0.5), light: transparent(TAB_HOVER_BACKGROUND, 0.7), - hc: null + hcDark: null, + hcLight: null }, localize('tabUnfocusedHoverBackground', "Tab background color in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_HOVER_FOREGROUND = registerColor('tab.hoverForeground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null, }, localize('tabHoverForeground', "Tab foreground color when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_FOREGROUND = registerColor('tab.unfocusedHoverForeground', { dark: transparent(TAB_HOVER_FOREGROUND, 0.5), light: transparent(TAB_HOVER_FOREGROUND, 0.5), - hc: null + hcDark: null, + hcLight: null }, localize('tabUnfocusedHoverForeground', "Tab foreground color in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -112,49 +127,57 @@ export const TAB_UNFOCUSED_HOVER_FOREGROUND = registerColor('tab.unfocusedHoverF export const TAB_BORDER = registerColor('tab.border', { dark: '#252526', light: '#F3F3F3', - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder, }, localize('tabBorder', "Border to separate tabs from each other. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_LAST_PINNED_BORDER = registerColor('tab.lastPinnedBorder', { dark: treeIndentGuidesStroke, light: treeIndentGuidesStroke, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('lastPinnedTabBorder', "Border to separate pinned tabs from other tabs. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_ACTIVE_BORDER = registerColor('tab.activeBorder', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('tabActiveBorder', "Border on the bottom of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER = registerColor('tab.unfocusedActiveBorder', { dark: transparent(TAB_ACTIVE_BORDER, 0.5), light: transparent(TAB_ACTIVE_BORDER, 0.7), - hc: null + hcDark: null, + hcLight: null }, localize('tabActiveUnfocusedBorder', "Border on the bottom of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_ACTIVE_BORDER_TOP = registerColor('tab.activeBorderTop', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: '#B5200D' }, localize('tabActiveBorderTop', "Border to the top of an active tab. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_BORDER_TOP = registerColor('tab.unfocusedActiveBorderTop', { dark: transparent(TAB_ACTIVE_BORDER_TOP, 0.5), light: transparent(TAB_ACTIVE_BORDER_TOP, 0.7), - hc: null + hcDark: null, + hcLight: '#B5200D' }, localize('tabActiveUnfocusedBorderTop', "Border to the top of an active tab in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_HOVER_BORDER = registerColor('tab.hoverBorder', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('tabHoverBorder', "Border to highlight tabs when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorder', { dark: transparent(TAB_HOVER_BORDER, 0.5), light: transparent(TAB_HOVER_BORDER, 0.7), - hc: null + hcDark: null, + hcLight: contrastBorder }, localize('tabUnfocusedHoverBorder', "Border to highlight tabs in an unfocused group when hovering. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -164,25 +187,29 @@ export const TAB_UNFOCUSED_HOVER_BORDER = registerColor('tab.unfocusedHoverBorde export const TAB_ACTIVE_MODIFIED_BORDER = registerColor('tab.activeModifiedBorder', { dark: '#3399CC', light: '#33AAEE', - hc: null + hcDark: null, + hcLight: contrastBorder }, localize('tabActiveModifiedBorder', "Border on the top of modified active tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_INACTIVE_MODIFIED_BORDER = registerColor('tab.inactiveModifiedBorder', { dark: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), - hc: Color.white + hcDark: Color.white, + hcLight: contrastBorder }, localize('tabInactiveModifiedBorder', "Border on the top of modified inactive tabs in an active group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_ACTIVE_MODIFIED_BORDER = registerColor('tab.unfocusedActiveModifiedBorder', { dark: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_ACTIVE_MODIFIED_BORDER, 0.7), - hc: Color.white + hcDark: Color.white, + hcLight: contrastBorder }, localize('unfocusedActiveModifiedBorder', "Border on the top of modified active tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocusedInactiveModifiedBorder', { dark: transparent(TAB_INACTIVE_MODIFIED_BORDER, 0.5), light: transparent(TAB_INACTIVE_MODIFIED_BORDER, 0.5), - hc: Color.white + hcDark: Color.white, + hcLight: contrastBorder }, localize('unfocusedINactiveModifiedBorder', "Border on the top of modified inactive tabs in an unfocused group. Tabs are the containers for editors in the editor area. Multiple tabs can be opened in one editor group. There can be multiple editor groups.")); //#endregion @@ -192,67 +219,78 @@ export const TAB_UNFOCUSED_INACTIVE_MODIFIED_BORDER = registerColor('tab.unfocus export const EDITOR_PANE_BACKGROUND = registerColor('editorPane.background', { dark: editorBackground, light: editorBackground, - hc: editorBackground + hcDark: editorBackground, + hcLight: editorBackground }, localize('editorPaneBackground', "Background color of the editor pane visible on the left and right side of the centered editor layout.")); export const EDITOR_GROUP_EMPTY_BACKGROUND = registerColor('editorGroup.emptyBackground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('editorGroupEmptyBackground', "Background color of an empty editor group. Editor groups are the containers of editors.")); export const EDITOR_GROUP_FOCUSED_EMPTY_BORDER = registerColor('editorGroup.focusedEmptyBorder', { dark: null, light: null, - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, localize('editorGroupFocusedEmptyBorder', "Border color of an empty editor group that is focused. Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_TABS_BACKGROUND = registerColor('editorGroupHeader.tabsBackground', { dark: '#252526', light: '#F3F3F3', - hc: null + hcDark: null, + hcLight: null }, localize('tabsContainerBackground', "Background color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_TABS_BORDER = registerColor('editorGroupHeader.tabsBorder', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('tabsContainerBorder', "Border color of the editor group title header when tabs are enabled. Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND = registerColor('editorGroupHeader.noTabsBackground', { dark: editorBackground, light: editorBackground, - hc: editorBackground + hcDark: editorBackground, + hcLight: editorBackground }, localize('editorGroupHeaderBackground', "Background color of the editor group title header when tabs are disabled (`\"workbench.editor.showTabs\": false`). Editor groups are the containers of editors.")); export const EDITOR_GROUP_HEADER_BORDER = registerColor('editorGroupHeader.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('editorTitleContainerBorder', "Border color of the editor group title header. Editor groups are the containers of editors.")); export const EDITOR_GROUP_BORDER = registerColor('editorGroup.border', { dark: '#444444', light: '#E7E7E7', - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('editorGroupBorder', "Color to separate multiple editor groups from each other. Editor groups are the containers of editors.")); export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBackground', { dark: Color.fromHex('#53595D').transparent(0.5), light: Color.fromHex('#2677CB').transparent(0.18), - hc: null + hcDark: null, + hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { dark: EDITOR_GROUP_BORDER, light: EDITOR_GROUP_BORDER, - hc: EDITOR_GROUP_BORDER + hcDark: EDITOR_GROUP_BORDER, + hcLight: EDITOR_GROUP_BORDER }, localize('sideBySideEditor.horizontalBorder', "Color to separate two editors from each other when shown side by side in an editor group from top to bottom.")); export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEditor.verticalBorder', { dark: EDITOR_GROUP_BORDER, light: EDITOR_GROUP_BORDER, - hc: EDITOR_GROUP_BORDER + hcDark: EDITOR_GROUP_BORDER, + hcLight: EDITOR_GROUP_BORDER }, localize('sideBySideEditor.verticalBorder', "Color to separate two editors from each other when shown side by side in an editor group from left to right.")); // < --- Panels --- > @@ -260,74 +298,86 @@ export const SIDE_BY_SIDE_EDITOR_VERTICAL_BORDER = registerColor('sideBySideEdit export const PANEL_BACKGROUND = registerColor('panel.background', { dark: editorBackground, light: editorBackground, - hc: editorBackground + hcDark: editorBackground, + hcLight: editorBackground }, localize('panelBackground', "Panel background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_BORDER = registerColor('panel.border', { dark: Color.fromHex('#808080').transparent(0.35), light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('panelBorder', "Panel border color to separate the panel from the editor. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_ACTIVE_TITLE_FOREGROUND = registerColor('panelTitle.activeForeground', { dark: '#E7E7E7', light: '#424242', - hc: Color.white + hcDark: Color.white, + hcLight: editorForeground }, localize('panelActiveTitleForeground', "Title color for the active panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_INACTIVE_TITLE_FOREGROUND = registerColor('panelTitle.inactiveForeground', { dark: transparent(PANEL_ACTIVE_TITLE_FOREGROUND, 0.6), light: transparent(PANEL_ACTIVE_TITLE_FOREGROUND, 0.75), - hc: Color.white + hcDark: Color.white, + hcLight: editorForeground }, localize('panelInactiveTitleForeground', "Title color for the inactive panel. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_ACTIVE_TITLE_BORDER = registerColor('panelTitle.activeBorder', { dark: PANEL_ACTIVE_TITLE_FOREGROUND, light: PANEL_ACTIVE_TITLE_FOREGROUND, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: '#B5200D' }, localize('panelActiveTitleBorder', "Border color for the active panel title. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { dark: null, light: Color.fromHex('#ddd'), - hc: null + hcDark: null, + hcLight: null }, localize('panelInputBorder', "Input box border for inputs in the panel.")); export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', { dark: PANEL_ACTIVE_TITLE_FOREGROUND, light: PANEL_ACTIVE_TITLE_FOREGROUND, - hc: PANEL_ACTIVE_TITLE_FOREGROUND, + hcDark: PANEL_ACTIVE_TITLE_FOREGROUND, + hcLight: PANEL_ACTIVE_TITLE_FOREGROUND }, localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', { dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hc: EDITOR_DRAG_AND_DROP_BACKGROUND, + hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, + hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND }, localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), - hc: null + hcDark: null, + hcLight: null, }, localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { dark: contrastBorder, light: contrastBorder, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('panelSectionHeaderBorder', "Panel section header border color used when multiple views are stacked vertically in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { dark: PANEL_BORDER, light: PANEL_BORDER, - hc: PANEL_BORDER + hcDark: PANEL_BORDER, + hcLight: PANEL_BORDER }, localize('panelSectionBorder', "Panel section border color used when multiple views are stacked horizontally in the panel. Panels are shown below the editor area and contain views like output and integrated terminal. Panel sections are views nested within the panels.")); // < --- Banner --- > @@ -335,19 +385,22 @@ export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { export const BANNER_BACKGROUND = registerColor('banner.background', { dark: listActiveSelectionBackground, light: darken(listActiveSelectionBackground, 0.3), - hc: listActiveSelectionBackground + hcDark: listActiveSelectionBackground, + hcLight: listActiveSelectionBackground }, localize('banner.background', "Banner background color. The banner is shown under the title bar of the window.")); export const BANNER_FOREGROUND = registerColor('banner.foreground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, - hc: listActiveSelectionForeground + hcDark: listActiveSelectionForeground, + hcLight: listActiveSelectionForeground }, localize('banner.foreground', "Banner foreground color. The banner is shown under the title bar of the window.")); export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { dark: editorInfoForeground, light: editorInfoForeground, - hc: editorInfoForeground + hcDark: editorInfoForeground, + hcLight: editorInfoForeground }, localize('banner.iconForeground', "Banner icon color. The banner is shown under the title bar of the window.")); // < --- Status --- > @@ -355,109 +408,127 @@ export const BANNER_ICON_FOREGROUND = registerColor('banner.iconForeground', { export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { dark: '#FFFFFF', light: '#FFFFFF', - hc: '#FFFFFF' + hcDark: '#FFFFFF', + hcLight: editorForeground }, localize('statusBarForeground', "Status bar foreground color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_FOREGROUND = registerColor('statusBar.noFolderForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, - hc: STATUS_BAR_FOREGROUND + hcDark: STATUS_BAR_FOREGROUND, + hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarNoFolderForeground', "Status bar foreground color when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_BACKGROUND = registerColor('statusBar.background', { dark: '#007ACC', light: '#007ACC', - hc: null + hcDark: null, + hcLight: null, }, localize('statusBarBackground', "Status bar background color when a workspace or folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_BACKGROUND = registerColor('statusBar.noFolderBackground', { dark: '#68217A', light: '#68217A', - hc: null + hcDark: null, + hcLight: null, }, localize('statusBarNoFolderBackground', "Status bar background color when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_BORDER = registerColor('statusBar.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('statusBarBorder', "Status bar border color separating to the sidebar and editor. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_FOCUS_BORDER = registerColor('statusBar.focusBorder', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, - hc: null, + hcDark: null, + hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarFocusBorder', "Status bar border color when focused on keyboard navigation. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_NO_FOLDER_BORDER = registerColor('statusBar.noFolderBorder', { dark: STATUS_BAR_BORDER, light: STATUS_BAR_BORDER, - hc: STATUS_BAR_BORDER + hcDark: STATUS_BAR_BORDER, + hcLight: STATUS_BAR_BORDER }, localize('statusBarNoFolderBorder', "Status bar border color separating to the sidebar and editor when no folder is opened. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_ACTIVE_BACKGROUND = registerColor('statusBarItem.activeBackground', { dark: Color.white.transparent(0.18), light: Color.white.transparent(0.18), - hc: Color.white.transparent(0.18) + hcDark: Color.white.transparent(0.18), + hcLight: Color.black.transparent(0.18) }, localize('statusBarItemActiveBackground', "Status bar item background color when clicking. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_FOCUS_BORDER = registerColor('statusBarItem.focusBorder', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, - hc: null, + hcDark: null, + hcLight: activeContrastBorder }, localize('statusBarItemFocusBorder', "Status bar item border color when focused on keyboard navigation. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hoverBackground', { dark: Color.white.transparent(0.12), light: Color.white.transparent(0.12), - hc: Color.white.transparent(0.12) + hcDark: Color.white.transparent(0.12), + hcLight: Color.black.transparent(0.12) }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { dark: Color.white.transparent(0.20), light: Color.white.transparent(0.20), - hc: Color.white.transparent(0.20) + hcDark: Color.white.transparent(0.20), + hcLight: Color.black.transparent(0.20) }, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, - hc: STATUS_BAR_FOREGROUND + hcDark: STATUS_BAR_FOREGROUND, + hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_BACKGROUND = registerColor('statusBarItem.prominentBackground', { dark: Color.black.transparent(0.5), light: Color.black.transparent(0.5), - hc: Color.black.transparent(0.5), + hcDark: Color.black.transparent(0.5), + hcLight: Color.black.transparent(0.5), }, localize('statusBarProminentItemBackground', "Status bar prominent items background color. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.prominentHoverBackground', { dark: Color.black.transparent(0.3), light: Color.black.transparent(0.3), - hc: Color.black.transparent(0.3), + hcDark: Color.black.transparent(0.3), + hcLight: null }, localize('statusBarProminentItemHoverBackground', "Status bar prominent items background color when hovering. Prominent items stand out from other status bar entries to indicate importance. Change mode `Toggle Tab Key Moves Focus` from command palette to see an example. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ERROR_ITEM_BACKGROUND = registerColor('statusBarItem.errorBackground', { dark: darken(errorForeground, .4), light: darken(errorForeground, .4), - hc: null, + hcDark: null, + hcLight: '#B5200D' }, localize('statusBarErrorItemBackground', "Status bar error items background color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ERROR_ITEM_FOREGROUND = registerColor('statusBarItem.errorForeground', { dark: Color.white, light: Color.white, - hc: Color.white, + hcDark: Color.white, + hcLight: Color.white }, localize('statusBarErrorItemForeground', "Status bar error items foreground color. Error items stand out from other status bar entries to indicate error conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_WARNING_ITEM_BACKGROUND = registerColor('statusBarItem.warningBackground', { dark: darken(editorWarningForeground, .4), light: darken(editorWarningForeground, .4), - hc: null, + hcDark: null, + hcLight: '#895503' }, localize('statusBarWarningItemBackground', "Status bar warning items background color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.warningForeground', { dark: Color.white, light: Color.white, - hc: Color.white, + hcDark: Color.white, + hcLight: Color.white }, localize('statusBarWarningItemForeground', "Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.")); @@ -466,61 +537,71 @@ export const STATUS_BAR_WARNING_ITEM_FOREGROUND = registerColor('statusBarItem.w export const ACTIVITY_BAR_BACKGROUND = registerColor('activityBar.background', { dark: '#333333', light: '#2C2C2C', - hc: '#000000' + hcDark: '#000000', + hcLight: '#FFFFFF' }, localize('activityBarBackground', "Activity bar background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_FOREGROUND = registerColor('activityBar.foreground', { dark: Color.white, light: Color.white, - hc: Color.white + hcDark: Color.white, + hcLight: editorForeground }, localize('activityBarForeground', "Activity bar item foreground color when it is active. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_INACTIVE_FOREGROUND = registerColor('activityBar.inactiveForeground', { dark: transparent(ACTIVITY_BAR_FOREGROUND, 0.4), light: transparent(ACTIVITY_BAR_FOREGROUND, 0.4), - hc: Color.white + hcDark: Color.white, + hcLight: editorForeground }, localize('activityBarInActiveForeground', "Activity bar item foreground color when it is inactive. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BORDER = registerColor('activityBar.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('activityBarBorder', "Activity bar border color separating to the side bar. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_ACTIVE_BORDER = registerColor('activityBar.activeBorder', { dark: ACTIVITY_BAR_FOREGROUND, light: ACTIVITY_BAR_FOREGROUND, - hc: null + hcDark: null, + hcLight: contrastBorder }, localize('activityBarActiveBorder', "Activity bar border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_ACTIVE_FOCUS_BORDER = registerColor('activityBar.activeFocusBorder', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: '#B5200D' }, localize('activityBarActiveFocusBorder', "Activity bar focus border color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeBackground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { dark: ACTIVITY_BAR_FOREGROUND, light: ACTIVITY_BAR_FOREGROUND, - hc: ACTIVITY_BAR_FOREGROUND, + hcDark: ACTIVITY_BAR_FOREGROUND, + hcLight: ACTIVITY_BAR_FOREGROUND, }, localize('activityBarDragAndDropBorder', "Drag and drop feedback color for the activity bar items. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.background', { dark: '#007ACC', light: '#007ACC', - hc: '#000000' + hcDark: '#000000', + hcLight: '#007ACC' }, localize('activityBarBadgeBackground', "Activity notification badge background color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.foreground', { dark: Color.white, light: Color.white, - hc: Color.white + hcDark: Color.white, + hcLight: Color.white }, localize('activityBarBadgeForeground', "Activity notification badge foreground color. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); @@ -529,25 +610,29 @@ export const ACTIVITY_BAR_BADGE_FOREGROUND = registerColor('activityBarBadge.for export const STATUS_BAR_HOST_NAME_BACKGROUND = registerColor('statusBarItem.remoteBackground', { dark: ACTIVITY_BAR_BADGE_BACKGROUND, light: ACTIVITY_BAR_BADGE_BACKGROUND, - hc: ACTIVITY_BAR_BADGE_BACKGROUND + hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, + hcLight: ACTIVITY_BAR_BADGE_BACKGROUND }, localize('statusBarItemHostBackground', "Background color for the remote indicator on the status bar.")); export const STATUS_BAR_HOST_NAME_FOREGROUND = registerColor('statusBarItem.remoteForeground', { dark: ACTIVITY_BAR_BADGE_FOREGROUND, light: ACTIVITY_BAR_BADGE_FOREGROUND, - hc: ACTIVITY_BAR_BADGE_FOREGROUND + hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, + hcLight: ACTIVITY_BAR_BADGE_FOREGROUND }, localize('statusBarItemHostForeground', "Foreground color for the remote indicator on the status bar.")); export const EXTENSION_BADGE_REMOTE_BACKGROUND = registerColor('extensionBadge.remoteBackground', { dark: ACTIVITY_BAR_BADGE_BACKGROUND, light: ACTIVITY_BAR_BADGE_BACKGROUND, - hc: ACTIVITY_BAR_BADGE_BACKGROUND + hcDark: ACTIVITY_BAR_BADGE_BACKGROUND, + hcLight: ACTIVITY_BAR_BADGE_BACKGROUND }, localize('extensionBadge.remoteBackground', "Background color for the remote badge in the extensions view.")); export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.remoteForeground', { dark: ACTIVITY_BAR_BADGE_FOREGROUND, light: ACTIVITY_BAR_BADGE_FOREGROUND, - hc: ACTIVITY_BAR_BADGE_FOREGROUND + hcDark: ACTIVITY_BAR_BADGE_FOREGROUND, + hcLight: ACTIVITY_BAR_BADGE_FOREGROUND }, localize('extensionBadge.remoteForeground', "Foreground color for the remote badge in the extensions view.")); @@ -556,49 +641,57 @@ export const EXTENSION_BADGE_REMOTE_FOREGROUND = registerColor('extensionBadge.r export const SIDE_BAR_BACKGROUND = registerColor('sideBar.background', { dark: '#252526', light: '#F3F3F3', - hc: '#000000' + hcDark: '#000000', + hcLight: '#FFFFFF' }, localize('sideBarBackground', "Side bar background color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_FOREGROUND = registerColor('sideBar.foreground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('sideBarForeground', "Side bar foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_BORDER = registerColor('sideBar.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('sideBarBorder', "Side bar border color on the side separating to the editor. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground', { dark: SIDE_BAR_FOREGROUND, light: SIDE_BAR_FOREGROUND, - hc: SIDE_BAR_FOREGROUND + hcDark: SIDE_BAR_FOREGROUND, + hcLight: SIDE_BAR_FOREGROUND }, localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', { dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hc: EDITOR_DRAG_AND_DROP_BACKGROUND, + hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, + hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND }, localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { dark: Color.fromHex('#808080').transparent(0.2), light: Color.fromHex('#808080').transparent(0.2), - hc: null + hcDark: null, + hcLight: null }, localize('sideBarSectionHeaderBackground', "Side bar section header background color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_FOREGROUND = registerColor('sideBarSectionHeader.foreground', { dark: SIDE_BAR_FOREGROUND, light: SIDE_BAR_FOREGROUND, - hc: SIDE_BAR_FOREGROUND + hcDark: SIDE_BAR_FOREGROUND, + hcLight: SIDE_BAR_FOREGROUND }, localize('sideBarSectionHeaderForeground', "Side bar section header foreground color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeader.border', { dark: contrastBorder, light: contrastBorder, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('sideBarSectionHeaderBorder', "Side bar section header border color. The side bar is the container for views like explorer and search. Side bar sections are views nested within the side bar.")); @@ -607,31 +700,36 @@ export const SIDE_BAR_SECTION_HEADER_BORDER = registerColor('sideBarSectionHeade export const TITLE_BAR_ACTIVE_FOREGROUND = registerColor('titleBar.activeForeground', { dark: '#CCCCCC', light: '#333333', - hc: '#FFFFFF' + hcDark: '#FFFFFF', + hcLight: '#292929' }, localize('titleBarActiveForeground', "Title bar foreground when the window is active.")); export const TITLE_BAR_INACTIVE_FOREGROUND = registerColor('titleBar.inactiveForeground', { dark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), - hc: null + hcDark: null, + hcLight: '#292929' }, localize('titleBarInactiveForeground', "Title bar foreground when the window is inactive.")); export const TITLE_BAR_ACTIVE_BACKGROUND = registerColor('titleBar.activeBackground', { dark: '#3C3C3C', light: '#DDDDDD', - hc: '#000000' + hcDark: '#000000', + hcLight: '#FFFFFF' }, localize('titleBarActiveBackground', "Title bar background when the window is active.")); export const TITLE_BAR_INACTIVE_BACKGROUND = registerColor('titleBar.inactiveBackground', { dark: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), - hc: null + hcDark: null, + hcLight: null, }, localize('titleBarInactiveBackground', "Title bar background when the window is inactive.")); export const TITLE_BAR_BORDER = registerColor('titleBar.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('titleBarBorder', "Title bar border color.")); // < --- Menubar --- > @@ -639,19 +737,22 @@ export const TITLE_BAR_BORDER = registerColor('titleBar.border', { export const MENUBAR_SELECTION_FOREGROUND = registerColor('menubar.selectionForeground', { dark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, - hc: TITLE_BAR_ACTIVE_FOREGROUND + hcDark: TITLE_BAR_ACTIVE_FOREGROUND, + hcLight: TITLE_BAR_ACTIVE_FOREGROUND, }, localize('menubarSelectionForeground', "Foreground color of the selected menu item in the menubar.")); export const MENUBAR_SELECTION_BACKGROUND = registerColor('menubar.selectionBackground', { dark: transparent(Color.white, 0.1), light: transparent(Color.black, 0.1), - hc: null + hcDark: null, + hcLight: null, }, localize('menubarSelectionBackground', "Background color of the selected menu item in the menubar.")); export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', { dark: null, light: null, - hc: activeContrastBorder + hcDark: activeContrastBorder, + hcLight: activeContrastBorder, }, localize('menubarSelectionBorder', "Border color of the selected menu item in the menubar.")); // < --- Notifications --- > @@ -659,77 +760,90 @@ export const MENUBAR_SELECTION_BORDER = registerColor('menubar.selectionBorder', export const NOTIFICATIONS_CENTER_BORDER = registerColor('notificationCenter.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('notificationCenterBorder', "Notifications center border color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_TOAST_BORDER = registerColor('notificationToast.border', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('notificationToastBorder', "Notification toast border color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_FOREGROUND = registerColor('notifications.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, - hc: editorWidgetForeground + hcDark: editorWidgetForeground, + hcLight: editorWidgetForeground }, localize('notificationsForeground', "Notifications foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_BACKGROUND = registerColor('notifications.background', { dark: editorWidgetBackground, light: editorWidgetBackground, - hc: editorWidgetBackground + hcDark: editorWidgetBackground, + hcLight: editorWidgetBackground }, localize('notificationsBackground', "Notifications background color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_LINKS = registerColor('notificationLink.foreground', { dark: textLinkForeground, light: textLinkForeground, - hc: textLinkForeground + hcDark: textLinkForeground, + hcLight: textLinkForeground }, localize('notificationsLink', "Notification links foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_CENTER_HEADER_FOREGROUND = registerColor('notificationCenterHeader.foreground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('notificationCenterHeaderForeground', "Notifications center header foreground color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_CENTER_HEADER_BACKGROUND = registerColor('notificationCenterHeader.background', { dark: lighten(NOTIFICATIONS_BACKGROUND, 0.3), light: darken(NOTIFICATIONS_BACKGROUND, 0.05), - hc: NOTIFICATIONS_BACKGROUND + hcDark: NOTIFICATIONS_BACKGROUND, + hcLight: NOTIFICATIONS_BACKGROUND }, localize('notificationCenterHeaderBackground', "Notifications center header background color. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_BORDER = registerColor('notifications.border', { dark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, light: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, - hc: NOTIFICATIONS_CENTER_HEADER_BACKGROUND + hcDark: NOTIFICATIONS_CENTER_HEADER_BACKGROUND, + hcLight: NOTIFICATIONS_CENTER_HEADER_BACKGROUND }, localize('notificationsBorder', "Notifications border color separating from other notifications in the notifications center. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_ERROR_ICON_FOREGROUND = registerColor('notificationsErrorIcon.foreground', { dark: editorErrorForeground, light: editorErrorForeground, - hc: editorErrorForeground + hcDark: editorErrorForeground, + hcLight: editorErrorForeground }, localize('notificationsErrorIconForeground', "The color used for the icon of error notifications. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_WARNING_ICON_FOREGROUND = registerColor('notificationsWarningIcon.foreground', { dark: editorWarningForeground, light: editorWarningForeground, - hc: editorWarningForeground + hcDark: editorWarningForeground, + hcLight: editorWarningForeground }, localize('notificationsWarningIconForeground', "The color used for the icon of warning notifications. Notifications slide in from the bottom right of the window.")); export const NOTIFICATIONS_INFO_ICON_FOREGROUND = registerColor('notificationsInfoIcon.foreground', { dark: editorInfoForeground, light: editorInfoForeground, - hc: editorInfoForeground + hcDark: editorInfoForeground, + hcLight: editorInfoForeground }, localize('notificationsInfoIconForeground', "The color used for the icon of info notifications. Notifications slide in from the bottom right of the window.")); export const WINDOW_ACTIVE_BORDER = registerColor('window.activeBorder', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('windowActiveBorder', "The color used for the border of the window when it is active. Only supported in the desktop client when using the custom title bar.")); export const WINDOW_INACTIVE_BORDER = registerColor('window.inactiveBorder', { dark: null, light: null, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('windowInactiveBorder', "The color used for the border of the window when it is inactive. Only supported in the desktop client when using the custom title bar.")); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index dd2a97af53d..20cd1212787 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,6 +27,7 @@ import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); @@ -346,13 +347,13 @@ export interface IViewContainerModel { readonly onDidMoveVisibleViewDescriptors: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef }>; isVisible(id: string): boolean; - setVisible(id: string, visible: boolean, size?: number): void; + setVisible(id: string, visible: boolean): void; isCollapsed(id: string): boolean; setCollapsed(id: string, collapsed: boolean): void; getSize(id: string): number | undefined; - setSize(id: string, size: number): void; + setSizes(newSizes: readonly { id: string; size: number }[]): void; move(from: string, to: string): void; } @@ -635,13 +636,6 @@ export interface IViewDescriptorService { // Custom views -export interface ITreeDataTransferItem { - asString(): Thenable; - value: any; -} - -export type ITreeDataTransfer = Map; - export interface ITreeView extends IDisposable { dataProvider: ITreeViewDataProvider | undefined; @@ -835,8 +829,8 @@ export interface ITreeViewDataProvider { export interface ITreeViewDragAndDropController { readonly dropMimeTypes: string[]; readonly dragMimeTypes: string[]; - handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; - handleDrop(elements: ITreeDataTransfer, target: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise; + handleDrag(sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; + handleDrop(elements: IDataTransfer, target: ITreeItem, token: CancellationToken, operationUuid?: string, sourceTreeId?: string, sourceTreeItemHandles?: string[]): Promise; } export interface IEditableData { diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index 0b58f3a5d4e..d551b2e12e4 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -7,11 +7,9 @@ import { CharCode } from 'vs/base/common/charCode'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; -export interface WebviewInitData { - readonly remote: { - readonly isRemote: boolean; - readonly authority: string | undefined; - }; +export interface WebviewRemoteInfo { + readonly isRemote: boolean; + readonly authority: string | undefined; } /** @@ -39,10 +37,7 @@ export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; * @param resource Uri of the resource to load. * @param remoteInfo Optional information about the remote that specifies where `resource` should be resolved from. */ -export function asWebviewUri( - resource: URI, - remoteInfo?: { authority: string | undefined; isRemote: boolean } -): URI { +export function asWebviewUri(resource: URI, remoteInfo?: WebviewRemoteInfo): URI { if (resource.scheme === Schemas.http || resource.scheme === Schemas.https) { return resource; } diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts index e32f0d66ecc..ec03a265487 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts @@ -51,12 +51,22 @@ export class AudioCueService extends Disposable implements IAudioCueService { await Promise.all(Array.from(sounds).map(sound => this.playSound(sound))); } + private getVolumeInPercent(): number { + let volume = this.configurationService.getValue('audioCues.volume'); + if (typeof volume !== 'number') { + return 50; + } + + return Math.max(Math.min(volume, 100), 0); + } + public async playSound(sound: Sound): Promise { const url = FileAccess.asBrowserUri( `vs/workbench/contrib/audioCues/browser/media/${sound.fileName}`, require ).toString(); const audio = new Audio(url); + audio.volume = this.getVolumeInPercent() / 100; try { try { diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index eb94dac36a1..4122bc5a7dd 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -36,6 +36,13 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'audioCues.enabled': { markdownDeprecationMessage: 'Deprecated. Use the specific setting for each audio cue instead (`audioCues.*`).', }, + 'audioCues.volume': { + 'description': localize('audioCues.volume', "The volume of the audio cues in percent (0-100)."), + 'type': 'number', + 'minimum': 0, + 'maximum': 100, + 'default': 50 + }, 'audioCues.lineHasBreakpoint': { 'description': localize('audioCues.lineHasBreakpoint', "Plays a sound when the active line has a breakpoint."), ...audioCueFeatureBase, diff --git a/src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.opus b/src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.opus index 40ec8c824d3..f540ca413af 100644 Binary files a/src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.opus and b/src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.opus differ diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index c184efe9bf8..a259de1b6d5 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -27,6 +27,7 @@ class BulkEdit { constructor( private readonly _label: string | undefined, + private readonly _code: string | undefined, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, private readonly _token: CancellationToken, @@ -108,13 +109,13 @@ class BulkEdit { private async _performFileEdits(edits: ResourceFileEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, confirmBeforeUndo: boolean, progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); + const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', undoRedoGroup, undoRedoSource, confirmBeforeUndo, progress, this._token, edits); await model.apply(); } private async _performTextEdits(edits: ResourceTextEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits); + const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._code || 'undoredo.workspaceEdit', this._editor, undoRedoGroup, undoRedoSource, progress, this._token, edits); await model.apply(); } @@ -199,6 +200,7 @@ export class BulkEditService implements IBulkEditService { const bulkEdit = this._instaService.createInstance( BulkEdit, label, + options?.code, codeEditor, options?.progress ?? Progress.None, options?.token ?? CancellationToken.None, diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index c28c9dfd7c9..b3247bba89d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -5,7 +5,7 @@ import { WorkspaceFileEditOptions } from 'vs/editor/common/languages'; -import { IFileService, FileSystemProviderCapabilities, IFileContent } from 'vs/platform/files/common/files'; +import { IFileService, FileSystemProviderCapabilities, IFileContent, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkingCopyFileService, IFileOperationUndoRedoInfo, IMoveOperation, ICopyOperation, IDeleteOperation, ICreateOperation, ICreateFileOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -233,7 +233,10 @@ class DeleteOperation implements IFileOperation { const undoes: CreateEdit[] = []; for (const edit of this._edits) { - if (!await this._fileService.exists(edit.oldUri)) { + let fileStat: IFileStatWithMetadata | undefined; + try { + fileStat = await this._fileService.resolve(edit.oldUri, { resolveMetadata: true }); + } catch (err) { if (!edit.options.ignoreIfNotExists) { throw new Error(`${edit.oldUri} does not exist and can not be deleted`); } @@ -249,15 +252,15 @@ class DeleteOperation implements IFileOperation { // read file contents for undo operation. when a file is too large it won't be restored let fileContent: IFileContent | undefined; - if (!edit.undoesCreate && !edit.options.folder) { + if (!edit.undoesCreate && !edit.options.folder && !(typeof edit.options.maxSize === 'number' && fileStat.size > edit.options.maxSize)) { try { fileContent = await this._fileService.readFile(edit.oldUri); } catch (err) { this._logService.critical(err); } } - if (!(typeof edit.options.maxSize === 'number' && fileContent && (fileContent?.size > edit.options.maxSize))) { - undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent?.value)); + if (fileContent !== undefined) { + undoes.push(new CreateEdit(edit.oldUri, edit.options, fileContent.value)); } } @@ -286,6 +289,7 @@ class FileUndoRedoElement implements IWorkspaceUndoRedoElement { constructor( readonly label: string, + readonly code: string, readonly operations: IFileOperation[], readonly confirmBeforeUndo: boolean ) { @@ -317,6 +321,7 @@ export class BulkFileEdits { constructor( private readonly _label: string, + private readonly _code: string, private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, private readonly _confirmBeforeUndo: boolean, @@ -390,6 +395,6 @@ export class BulkFileEdits { this._progress.report(undefined); } - this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); + this._undoRedoService.pushElement(new FileUndoRedoElement(this._label, this._code, undoOperations, this._confirmBeforeUndo), this._undoRedoGroup, this._undoRedoSource); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 737c8d2a00d..b0f3ba726bb 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -144,6 +144,7 @@ export class BulkTextEdits { constructor( private readonly _label: string, + private readonly _code: string, private readonly _editor: ICodeEditor | undefined, private readonly _undoRedoGroup: UndoRedoGroup, private readonly _undoRedoSource: UndoRedoSource | undefined, @@ -249,7 +250,7 @@ export class BulkTextEdits { // This edit touches a single model => keep things simple const task = tasks[0]; if (!task.isNoOp()) { - const singleModelEditStackElement = new SingleModelEditStackElement(task.model, task.getBeforeCursorState()); + const singleModelEditStackElement = new SingleModelEditStackElement(this._label, this._code, task.model, task.getBeforeCursorState()); this._undoRedoService.pushElement(singleModelEditStackElement, this._undoRedoGroup, this._undoRedoSource); task.apply(); singleModelEditStackElement.close(); @@ -259,7 +260,8 @@ export class BulkTextEdits { // prepare multi model undo element const multiModelEditStackElement = new MultiModelEditStackElement( this._label, - tasks.map(t => new SingleModelEditStackElement(t.model, t.getBeforeCursorState())) + this._code, + tasks.map(t => new SingleModelEditStackElement(this._label, this._code, t.model, t.getBeforeCursorState())) ); this._undoRedoService.pushElement(multiModelEditStackElement, this._undoRedoGroup, this._undoRedoSource); for (const task of tasks) { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css index 7e994763d64..e113ad073ff 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css @@ -82,7 +82,9 @@ } .monaco-workbench .bulk-edit-panel .monaco-tl-contents.category .uri-icon, -.monaco-workbench .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon { +.monaco-workbench .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon, +.monaco-workbench.hc-light .bulk-edit-panel .monaco-tl-contents.category .uri-icon, +.monaco-workbench.hc-light .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon { background-repeat: no-repeat; background-image: var(--background-light); background-position: left center; diff --git a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts index 86ef656cdbc..89a7f95c827 100644 --- a/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts +++ b/src/vs/workbench/contrib/codeActions/browser/codeActionsContribution.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten } from 'vs/base/common/arrays'; import { Emitter } from 'vs/base/common/event'; import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -64,7 +63,7 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon super(); codeActionsExtensionPoint.setHandler(extensionPoints => { - this._contributedCodeActions = flatten(extensionPoints.map(x => x.value)); + this._contributedCodeActions = extensionPoints.map(x => x.value).flat(); this.updateConfigurationSchema(this._contributedCodeActions); this._onDidChangeContributions.fire(); }); @@ -135,7 +134,7 @@ export class CodeActionsContribution extends Disposable implements IWorkbenchCon }; const getActions = (ofKind: CodeActionKind): ContributedCodeAction[] => { - const allActions = flatten(this._contributedCodeActions.map(desc => desc.actions.slice())); + const allActions = this._contributedCodeActions.map(desc => desc.actions).flat(); const out = new Map(); for (const action of allActions) { diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index e2b59f9d4b2..86b60ef52a3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -32,7 +32,7 @@ import { registerThemingParticipant } from 'vs/platform/theme/common/themeServic import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; const CONTEXT_ACCESSIBILITY_WIDGET_VISIBLE = new RawContextKey('accessibilityHelpWidgetVisible', false); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css index 2be494fd722..bdd496042ce 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.css @@ -27,6 +27,10 @@ transition: top 200ms linear; } +.monaco-workbench.reduce-motion .monaco-editor .find-widget { + transition: top 0ms linear; +} + .monaco-workbench .simple-find-part.visible { visibility: visible; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 4dd4f7e3bb3..e3111f08d6c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -30,7 +30,7 @@ import { ColorThemeData, TokenStyleDefinitions, TokenStyleDefinition, TextMateTh import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelService'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; import { Schemas } from 'vs/base/common/network'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -675,7 +675,7 @@ registerEditorAction(InspectEditorTokens); registerThemingParticipant((theme, collector) => { const border = theme.getColor(editorHoverBorder); if (border) { - let borderWidth = theme.type === ColorScheme.HIGH_CONTRAST ? 2 : 1; + let borderWidth = isHighContrast(theme.type) ? 2 : 1; collector.addRule(`.monaco-editor .token-inspect-widget { border: ${borderWidth}px solid ${border}; }`); collector.addRule(`.monaco-editor .token-inspect-widget .tiw-metadata-separator { background-color: ${border}; }`); } diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts new file mode 100644 index 00000000000..3fc12217f70 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Color } from 'vs/base/common/color'; +import * as languages from 'vs/editor/common/languages'; +import * as nls from 'vs/nls'; +import { contrastBorder, editorWarningForeground, editorWidgetForeground, registerColor } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme } from 'vs/platform/theme/common/themeService'; + +export const resolvedCommentBorder = registerColor('comments.resolved.border', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('resolvedCommentBorder', 'Color of borders and arrow for resolved comments.')); +export const unresolvedCommentBorder = registerColor('comments.unresolved.border', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); + +const commentThreadStateColors = new Map([ + [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], + [languages.CommentThreadState.Resolved, resolvedCommentBorder], +]); + +export const commentThreadStateColorVar = '--comment-thread-state-color'; + +export function getCommentThreadStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { + const colorId = thread.state !== undefined ? commentThreadStateColors.get(thread.state) : undefined; + return colorId !== undefined ? theme.getColor(colorId) : undefined; +} diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 8382e6c4318..71be0f04c36 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -13,9 +13,10 @@ import { themeColorFromId } from 'vs/platform/theme/common/themeService'; const overviewRulerDefault = new Color(new RGBA(197, 197, 197, 1)); -export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: overviewRulerDefault, light: overviewRulerDefault, hc: overviewRulerDefault }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges.')); +export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: overviewRulerDefault, light: overviewRulerDefault, hcDark: overviewRulerDefault, hcLight: overviewRulerDefault }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges.')); export class CommentGlyphWidget { + public static description = 'comment-glyph-widget'; private _lineNumber!: number; private _editor: ICodeEditor; private commentsDecorations: string[] = []; @@ -29,7 +30,7 @@ export class CommentGlyphWidget { private createDecorationOptions(): ModelDecorationOptions { const decorationOptions: IModelDecorationOptions = { - description: 'comment-glyph-widget', + description: CommentGlyphWidget.description, isWholeLine: true, overviewRuler: { color: themeColorFromId(overviewRulerCommentingRangeForeground), diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts index b9512d03f6b..2f5e9a671f8 100644 --- a/src/vs/workbench/contrib/comments/browser/commentMenus.ts +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -7,7 +7,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IAction } from 'vs/base/common/actions'; -import { Comment, CommentThread } from 'vs/editor/common/languages'; +import { Comment } from 'vs/editor/common/languages'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; export class CommentMenus implements IDisposable { @@ -15,11 +15,11 @@ export class CommentMenus implements IDisposable { @IMenuService private readonly menuService: IMenuService ) { } - getCommentThreadTitleActions(commentThread: CommentThread, contextKeyService: IContextKeyService): IMenu { + getCommentThreadTitleActions(contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.CommentThreadTitle, contextKeyService); } - getCommentThreadActions(commentThread: CommentThread, contextKeyService: IContextKeyService): IMenu { + getCommentThreadActions(contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.CommentThreadActions, contextKeyService); } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index f8c122ba72f..fbd43cd3aed 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -25,7 +25,6 @@ import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { ToggleReactionsAction, ReactionAction, ReactionActionViewItem } from './reactionsAction'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { MenuItemAction, SubmenuItemAction, IMenu } from 'vs/platform/actions/common/actions'; import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -39,8 +38,10 @@ import { MarshalledId } from 'vs/base/common/marshallingIds'; import { TimestampWidget } from 'vs/workbench/contrib/comments/browser/timestamp'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { IRange } from 'vs/editor/common/core/range'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -export class CommentNode extends Disposable { +export class CommentNode extends Disposable { private _domNode: HTMLElement; private _body: HTMLElement; private _md: HTMLElement | undefined; @@ -65,7 +66,7 @@ export class CommentNode extends Disposable { protected toolbar: ToolBar | undefined; private _commentFormActions: CommentFormActions | null = null; - private readonly _onDidClick = new Emitter(); + private readonly _onDidClick = new Emitter>(); public get domNode(): HTMLElement { return this._domNode; @@ -74,11 +75,10 @@ export class CommentNode extends Disposable { public isEditing: boolean = false; constructor( - private commentThread: languages.CommentThread, + private commentThread: languages.CommentThread, public comment: languages.Comment, private owner: string, private resource: URI, - private parentEditor: ICodeEditor, private parentThread: ICommentThreadWidget, private markdownRenderer: MarkdownRenderer, @IThemeService private themeService: IThemeService, @@ -133,7 +133,7 @@ export class CommentNode extends Disposable { } } - public get onDidClick(): Event { + public get onDidClick(): Event> { return this._onDidClick.event; } @@ -375,7 +375,7 @@ export class CommentNode extends Disposable { private createCommentEditor(editContainer: HTMLElement): void { const container = dom.append(editContainer, dom.$('.edit-textarea')); - this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(), this.parentEditor, this.parentThread); + this._commentEditor = this.instantiationService.createInstance(SimpleCommentEditor, container, SimpleCommentEditor.getEditorOptions(), this.parentThread); const resource = URI.parse(`comment:commentinput-${this.comment.uniqueIdInThread}-${Date.now()}.md`); this._commentEditorModel = this.modelService.createModel('', this.languageService.createByFilepathOrFirstLine(resource), resource, false); diff --git a/src/vs/workbench/contrib/comments/browser/commentReply.ts b/src/vs/workbench/contrib/comments/browser/commentReply.ts new file mode 100644 index 00000000000..6b9c90cc2ba --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentReply.ts @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { IAction } from 'vs/base/common/actions'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IRange } from 'vs/editor/common/core/range'; +import * as languages from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; +import { IModelService } from 'vs/editor/common/services/model'; +import * as nls from 'vs/nls'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; +import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { SimpleCommentEditor } from './simpleCommentEditor'; + +const COMMENT_SCHEME = 'comment'; +let INMEM_MODEL_ID = 0; +export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; + +export class CommentReply extends Disposable { + commentEditor: ICodeEditor; + form: HTMLElement; + commentEditorIsEmpty: IContextKey; + private _error!: HTMLElement; + private _formActions: HTMLElement | null; + private _commentThreadDisposables: IDisposable[] = []; + private _commentFormActions!: CommentFormActions; + private _reviewThreadReplyButton!: HTMLElement; + + constructor( + readonly owner: string, + container: HTMLElement, + private _commentThread: languages.CommentThread, + private _scopedInstatiationService: IInstantiationService, + private _contextKeyService: IContextKeyService, + private _commentMenus: CommentMenus, + private _commentOptions: languages.CommentOptions | undefined, + private _pendingComment: string | null, + private _parentThread: ICommentThreadWidget, + private _actionRunDelegate: (() => void) | null, + @ICommentService private commentService: ICommentService, + @ILanguageService private languageService: ILanguageService, + @IModelService private modelService: IModelService, + @IThemeService private themeService: IThemeService, + ) { + super(); + + this.form = dom.append(container, dom.$('.comment-form')); + this.commentEditor = this._register(this._scopedInstatiationService.createInstance(SimpleCommentEditor, this.form, SimpleCommentEditor.getEditorOptions(), this._parentThread)); + this.commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); + this.commentEditorIsEmpty.set(!this._pendingComment); + + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; + const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); + const params = JSON.stringify({ + extensionId: this._commentThread.extensionId, + commentThreadId: this._commentThread.threadId + }); + + let resource = URI.parse(`${COMMENT_SCHEME}://${this._commentThread.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. + let commentController = this.commentService.getCommentController(owner); + if (commentController) { + resource = resource.with({ authority: commentController.id }); + } + + const model = this.modelService.createModel(this._pendingComment || '', this.languageService.createByFilepathOrFirstLine(resource), resource, false); + this._register(model); + this.commentEditor.setModel(model); + + this._register((this.commentEditor.getModel()!.onDidChangeContent(() => { + this.setCommentEditorDecorations(); + this.commentEditorIsEmpty?.set(!this.commentEditor.getValue()); + }))); + + this.createTextModelListener(this.commentEditor, this.form); + + this.setCommentEditorDecorations(); + + // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments + if (hasExistingComments) { + this.createReplyButton(this.commentEditor, this.form); + } else { + if (this._commentThread.comments && this._commentThread.comments.length === 0) { + this.expandReplyArea(); + } + } + this._error = dom.append(this.form, dom.$('.validation-error.hidden')); + + this._formActions = dom.append(this.form, dom.$('.form-actions')); + this.createCommentWidgetActions(this._formActions, model); + } + + public updateCommentThread(commentThread: languages.CommentThread) { + const isReplying = this.commentEditor.hasTextFocus(); + + if (!this._reviewThreadReplyButton) { + this.createReplyButton(this.commentEditor, this.form); + } + + if (this._commentThread.comments && this._commentThread.comments.length === 0) { + this.expandReplyArea(); + } + + if (isReplying) { + this.commentEditor.focus(); + } + } + + public getPendingComment(): string | null { + let model = this.commentEditor.getModel(); + + if (model && model.getValueLength() > 0) { // checking length is cheap + return model.getValue(); + } + + return null; + } + + public layout(widthInPixel: number) { + this.commentEditor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); + } + + public focusIfNeeded() { + if (!this._commentThread.comments || !this._commentThread.comments.length) { + this.commentEditor.focus(); + } else if (this.commentEditor.getModel()!.getValueLength() > 0) { + this.expandReplyArea(); + } + } + + public focusCommentEditor() { + this.commentEditor.focus(); + } + + public getCommentModel() { + return this.commentEditor.getModel()!; + } + + public updateCanReply() { + if (!this._commentThread.canReply) { + this.form.style.display = 'none'; + } else { + this.form.style.display = 'block'; + } + } + + async submitComment(): Promise { + if (this._commentFormActions) { + this._commentFormActions.triggerDefaultAction(); + } + } + + setCommentEditorDecorations() { + const model = this.commentEditor.getModel(); + if (model) { + const valueLength = model.getValueLength(); + const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; + const placeholder = valueLength > 0 + ? '' + : hasExistingComments + ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) + : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); + const decorations = [{ + range: { + startLineNumber: 0, + endLineNumber: 0, + startColumn: 0, + endColumn: 1 + }, + renderOptions: { + after: { + contentText: placeholder, + color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` + } + } + }]; + + this.commentEditor.setDecorations('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); + } + } + + private createTextModelListener(commentEditor: ICodeEditor, commentForm: HTMLElement) { + this._commentThreadDisposables.push(commentEditor.onDidFocusEditorWidget(() => { + this._commentThread.input = { + uri: commentEditor.getModel()!.uri, + value: commentEditor.getValue() + }; + this.commentService.setActiveCommentThread(this._commentThread); + })); + + this._commentThreadDisposables.push(commentEditor.getModel()!.onDidChangeContent(() => { + let modelContent = commentEditor.getValue(); + if (this._commentThread.input && this._commentThread.input.uri === commentEditor.getModel()!.uri && this._commentThread.input.value !== modelContent) { + let newInput: languages.CommentInput = this._commentThread.input; + newInput.value = modelContent; + this._commentThread.input = newInput; + } + this.commentService.setActiveCommentThread(this._commentThread); + })); + + this._commentThreadDisposables.push(this._commentThread.onDidChangeInput(input => { + let thread = this._commentThread; + + if (thread.input && thread.input.uri !== commentEditor.getModel()!.uri) { + return; + } + if (!input) { + return; + } + + if (commentEditor.getValue() !== input.value) { + commentEditor.setValue(input.value); + + if (input.value === '') { + this._pendingComment = ''; + commentForm.classList.remove('expand'); + commentEditor.getDomNode()!.style.outline = ''; + this._error.textContent = ''; + this._error.classList.add('hidden'); + } + } + })); + } + + /** + * Command based actions. + */ + private createCommentWidgetActions(container: HTMLElement, model: ITextModel) { + const menu = this._commentMenus.getCommentThreadActions(this._contextKeyService); + + this._register(menu); + this._register(menu.onDidChange(() => { + this._commentFormActions.setActions(menu); + })); + + this._commentFormActions = new CommentFormActions(container, async (action: IAction) => { + if (this._actionRunDelegate) { + this._actionRunDelegate(); + } + + action.run({ + thread: this._commentThread, + text: this.commentEditor.getValue(), + $mid: MarshalledId.CommentThreadReply + }); + + this.hideReplyArea(); + }, this.themeService); + + this._commentFormActions.setActions(menu); + } + + private expandReplyArea() { + if (!this.form.classList.contains('expand')) { + this.form.classList.add('expand'); + this.commentEditor.focus(); + this.commentEditor.layout(); + } + } + + private hideReplyArea() { + this.commentEditor.setValue(''); + this.commentEditor.getDomNode()!.style.outline = ''; + this._pendingComment = ''; + this.form.classList.remove('expand'); + this._error.textContent = ''; + this._error.classList.add('hidden'); + } + + private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { + this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); + this._reviewThreadReplyButton.title = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); + + this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); + // bind click/escape actions for reviewThreadReplyButton and textArea + this._register(dom.addDisposableListener(this._reviewThreadReplyButton, 'click', _ => this.expandReplyArea())); + this._register(dom.addDisposableListener(this._reviewThreadReplyButton, 'focus', _ => this.expandReplyArea())); + + commentEditor.onDidBlurEditorWidget(() => { + if (commentEditor.getModel()!.getValueLength() === 0 && commentForm.classList.contains('expand')) { + commentForm.classList.remove('expand'); + } + }); + } + +} diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index a4ce66f7391..eca82718296 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -12,6 +12,7 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; export const ICommentService = createDecorator('commentService'); @@ -25,6 +26,13 @@ export interface ICommentInfo extends CommentInfo { label?: string; } +export interface INotebookCommentInfo { + extensionId?: string; + threads: CommentThread[]; + owner: string; + label?: string; +} + export interface IWorkspaceCommentThreadsEvent { ownerId: string; commentThreads: CommentThread[]; @@ -44,6 +52,7 @@ export interface ICommentController { deleteCommentThreadMain(commentThreadId: string): void; toggleReaction(uri: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction, token: CancellationToken): Promise; getDocumentComments(resource: URI, token: CancellationToken): Promise; + getNotebookComments(resource: URI, token: CancellationToken): Promise; getCommentingRanges(resource: URI, token: CancellationToken): Promise; } @@ -58,7 +67,7 @@ export interface ICommentService { readonly onDidSetDataProvider: Event; readonly onDidDeleteDataProvider: Event; setDocumentComments(resource: URI, commentInfos: ICommentInfo[]): void; - setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; + setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void; removeWorkspaceComments(owner: string): void; registerCommentController(owner: string, commentControl: ICommentController): void; unregisterCommentController(owner: string): void; @@ -66,14 +75,15 @@ export interface ICommentService { createCommentThreadTemplate(owner: string, resource: URI, range: Range): void; updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise; getCommentMenus(owner: string): CommentMenus; - updateComments(ownerId: string, event: CommentThreadChangedEvent): void; + updateComments(ownerId: string, event: CommentThreadChangedEvent): void; disposeCommentThread(ownerId: string, threadId: string): void; - getComments(resource: URI): Promise<(ICommentInfo | null)[]>; + getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]>; + getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]>; updateCommentingRanges(ownerId: string): void; getCommentingRanges(resource: URI): Promise; hasReactionHandler(owner: string): boolean; - toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; - setActiveCommentThread(commentThread: CommentThread | null): void; + toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; + setActiveCommentThread(commentThread: CommentThread | null): void; } export class CommentService extends Disposable implements ICommentService { @@ -185,7 +195,7 @@ export class CommentService extends Disposable implements ICommentService { return menu; } - updateComments(ownerId: string, event: CommentThreadChangedEvent): void { + updateComments(ownerId: string, event: CommentThreadChangedEvent): void { const evt: ICommentThreadChangedEvent = Object.assign({}, event, { owner: ownerId }); this._onDidUpdateCommentThreads.fire(evt); } @@ -214,7 +224,7 @@ export class CommentService extends Disposable implements ICommentService { return false; } - async getComments(resource: URI): Promise<(ICommentInfo | null)[]> { + async getDocumentComments(resource: URI): Promise<(ICommentInfo | null)[]> { let commentControlResult: Promise[] = []; this._commentControls.forEach(control => { @@ -227,6 +237,19 @@ export class CommentService extends Disposable implements ICommentService { return Promise.all(commentControlResult); } + async getNotebookComments(resource: URI): Promise<(INotebookCommentInfo | null)[]> { + let commentControlResult: Promise[] = []; + + this._commentControls.forEach(control => { + commentControlResult.push(control.getNotebookComments(resource, CancellationToken.None) + .catch(_ => { + return null; + })); + }); + + return Promise.all(commentControlResult); + } + async getCommentingRanges(resource: URI): Promise { let commentControlResult: Promise[] = []; diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts new file mode 100644 index 00000000000..9545d7afcc9 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import * as nls from 'vs/nls'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import * as languages from 'vs/editor/common/languages'; +import { Emitter } from 'vs/base/common/event'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { CommentNode } from 'vs/workbench/contrib/comments/browser/commentNode'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { IMarkdownRendererOptions, MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { IRange } from 'vs/editor/common/core/range'; + +export class CommentThreadBody extends Disposable { + private _commentsElement!: HTMLElement; + private _commentElements: CommentNode[] = []; + private _resizeObserver: any; + private _focusedComment: number | undefined = undefined; + private _onDidResize = new Emitter(); + onDidResize = this._onDidResize.event; + + private _commentDisposable = new Map, IDisposable>(); + private _markdownRenderer: MarkdownRenderer; + + get length() { + return this._commentThread.comments ? this._commentThread.comments.length : 0; + } + + get activeComment() { + return this._commentElements.filter(node => node.isEditing)[0]; + } + + + constructor( + readonly owner: string, + readonly parentResourceUri: URI, + readonly container: HTMLElement, + private _options: IMarkdownRendererOptions, + private _commentThread: languages.CommentThread, + private _scopedInstatiationService: IInstantiationService, + private _parentCommentThreadWidget: ICommentThreadWidget, + @ICommentService private commentService: ICommentService, + @IOpenerService private openerService: IOpenerService, + @ILanguageService private languageService: ILanguageService, + ) { + super(); + + this._register(dom.addDisposableListener(container, dom.EventType.FOCUS_IN, e => { + // TODO @rebornix, limit T to IRange | ICellRange + this.commentService.setActiveCommentThread(this._commentThread); + })); + + this._markdownRenderer = this._register(new MarkdownRenderer(this._options, this.languageService, this.openerService)); + } + + focus() { + this._commentsElement.focus(); + } + + display() { + this._commentsElement = dom.append(this.container, dom.$('div.comments-container')); + this._commentsElement.setAttribute('role', 'presentation'); + this._commentsElement.tabIndex = 0; + this._updateAriaLabel(); + + this._register(dom.addDisposableListener(this._commentsElement, dom.EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + if (event.equals(KeyCode.UpArrow) || event.equals(KeyCode.DownArrow)) { + const moveFocusWithinBounds = (change: number): number => { + if (this._focusedComment === undefined && change >= 0) { return 0; } + if (this._focusedComment === undefined && change < 0) { return this._commentElements.length - 1; } + let newIndex = this._focusedComment! + change; + return Math.min(Math.max(0, newIndex), this._commentElements.length - 1); + }; + + this._setFocusedComment(event.equals(KeyCode.UpArrow) ? moveFocusWithinBounds(-1) : moveFocusWithinBounds(1)); + } + })); + + this._commentElements = []; + if (this._commentThread.comments) { + for (const comment of this._commentThread.comments) { + const newCommentNode = this.createNewCommentNode(comment); + + this._commentElements.push(newCommentNode); + this._commentsElement.appendChild(newCommentNode.domNode); + if (comment.mode === languages.CommentMode.Editing) { + newCommentNode.switchToEditMode(); + } + } + } + + this._resizeObserver = new MutationObserver(this._refresh.bind(this)); + + this._resizeObserver.observe(this.container, { + attributes: true, + childList: true, + characterData: true, + subtree: true + }); + } + + private _refresh() { + let dimensions = dom.getClientArea(this.container); + this._onDidResize.fire(dimensions); + } + + getDimensions() { + return dom.getClientArea(this.container); + } + + layout() { + this._commentElements.forEach(element => { + element.layout(); + }); + } + + getCommentCoords(commentUniqueId: number): { thread: dom.IDomNodePagePosition; comment: dom.IDomNodePagePosition } | undefined { + let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId); + if (matchedNode && matchedNode.length) { + const commentThreadCoords = dom.getDomNodePagePosition(this._commentElements[0].domNode); + const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode); + return { + thread: commentThreadCoords, + comment: commentCoords + }; + } + + return; + } + + updateCommentThread(commentThread: languages.CommentThread) { + const oldCommentsLen = this._commentElements.length; + const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; + + let commentElementsToDel: CommentNode[] = []; + let commentElementsToDelIndex: number[] = []; + for (let i = 0; i < oldCommentsLen; i++) { + let comment = this._commentElements[i].comment; + let newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : []; + + if (newComment.length) { + this._commentElements[i].update(newComment[0]); + } else { + commentElementsToDelIndex.push(i); + commentElementsToDel.push(this._commentElements[i]); + } + } + + // del removed elements + for (let i = commentElementsToDel.length - 1; i >= 0; i--) { + const commentToDelete = commentElementsToDel[i]; + this._commentDisposable.get(commentToDelete)?.dispose(); + this._commentDisposable.delete(commentToDelete); + + this._commentElements.splice(commentElementsToDelIndex[i], 1); + this._commentsElement.removeChild(commentToDelete.domNode); + } + + + let lastCommentElement: HTMLElement | null = null; + let newCommentNodeList: CommentNode[] = []; + let newCommentsInEditMode: CommentNode[] = []; + for (let i = newCommentsLen - 1; i >= 0; i--) { + let currentComment = commentThread.comments![i]; + let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread); + if (oldCommentNode.length) { + lastCommentElement = oldCommentNode[0].domNode; + newCommentNodeList.unshift(oldCommentNode[0]); + } else { + const newElement = this.createNewCommentNode(currentComment); + + newCommentNodeList.unshift(newElement); + if (lastCommentElement) { + this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); + lastCommentElement = newElement.domNode; + } else { + this._commentsElement.appendChild(newElement.domNode); + lastCommentElement = newElement.domNode; + } + + if (currentComment.mode === languages.CommentMode.Editing) { + newElement.switchToEditMode(); + newCommentsInEditMode.push(newElement); + } + } + } + + this._commentThread = commentThread; + this._commentElements = newCommentNodeList; + + if (newCommentsInEditMode.length) { + const lastIndex = this._commentElements.indexOf(newCommentsInEditMode[newCommentsInEditMode.length - 1]); + this._focusedComment = lastIndex; + } + + this._updateAriaLabel(); + this._setFocusedComment(this._focusedComment); + } + + private _updateAriaLabel() { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thead with {0} comments. {1}.", + this._commentThread.comments?.length, this._commentThread.label); + } + + private _setFocusedComment(value: number | undefined) { + if (this._focusedComment !== undefined) { + this._commentElements[this._focusedComment]?.setFocus(false); + } + + if (this._commentElements.length === 0 || value === undefined) { + this._focusedComment = undefined; + } else { + this._focusedComment = Math.min(value, this._commentElements.length - 1); + this._commentElements[this._focusedComment].setFocus(true); + } + } + + private createNewCommentNode(comment: languages.Comment): CommentNode { + let newCommentNode = this._scopedInstatiationService.createInstance(CommentNode, + this._commentThread, + comment, + this.owner, + this.parentResourceUri, + this._parentCommentThreadWidget, + this._markdownRenderer) as unknown as CommentNode; + + this._register(newCommentNode); + this._commentDisposable.set(newCommentNode, newCommentNode.onDidClick(clickedNode => + this._setFocusedComment(this._commentElements.findIndex(commentNode => commentNode.comment.uniqueIdInThread === clickedNode.comment.uniqueIdInThread)) + )); + + return newCommentNode; + } + + public override dispose(): void { + super.dispose(); + + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } + + this._commentDisposable.forEach(v => v.dispose()); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts new file mode 100644 index 00000000000..1c16bb4ffdb --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadHeader.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as strings from 'vs/base/common/strings'; +import * as languages from 'vs/editor/common/languages'; +import { IRange } from 'vs/editor/common/core/range'; +import * as nls from 'vs/nls'; +import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; + +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); + + +export class CommentThreadHeader extends Disposable { + private _headElement: HTMLElement; + private _headingLabel!: HTMLElement; + private _actionbarWidget!: ActionBar; + private _collapseAction!: Action; + + constructor( + container: HTMLElement, + private _delegate: { collapse: () => void }, + private _commentMenus: CommentMenus, + private _commentThread: languages.CommentThread, + private _contextKeyService: IContextKeyService, + private instantiationService: IInstantiationService + ) { + super(); + this._headElement = dom.$('.head'); + container.appendChild(this._headElement); + this._fillHead(); + } + + protected _fillHead(): void { + let titleElement = dom.append(this._headElement, dom.$('.review-title')); + + this._headingLabel = dom.append(titleElement, dom.$('span.filename')); + this.createThreadLabel(); + + const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); + this._actionbarWidget = new ActionBar(actionsContainer, { + actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) + }); + + this._register(this._actionbarWidget); + + this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this._delegate.collapse()); + + const menu = this._commentMenus.getCommentThreadTitleActions(this._contextKeyService); + this.setActionBarActions(menu); + + this._register(menu); + this._register(menu.onDidChange(e => { + this.setActionBarActions(menu); + })); + + this._actionbarWidget.context = this._commentThread; + } + + private setActionBarActions(menu: IMenu): void { + const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <(MenuItemAction | SubmenuItemAction)[]>[]); + this._actionbarWidget.clear(); + this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true }); + } + + updateCommentThread(commentThread: languages.CommentThread) { + this._commentThread = commentThread; + + this._actionbarWidget.context = this._commentThread; + this.createThreadLabel(); + } + + createThreadLabel() { + let label: string | undefined; + label = this._commentThread.label; + + if (label === undefined) { + if (!(this._commentThread.comments && this._commentThread.comments.length)) { + label = nls.localize('startThread', "Start discussion"); + } + } + + if (label) { + this._headingLabel.textContent = strings.escape(label); + this._headingLabel.setAttribute('aria-label', label); + } + } + + updateHeight(headHeight: number) { + this._headElement.style.height = `${headHeight}px`; + this._headElement.style.lineHeight = this._headElement.style.height; + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 9308b4b2e2b..ac743f0f7bb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -3,179 +3,103 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/review'; import * as dom from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { Action, IAction } from 'vs/base/common/actions'; -import { Color } from 'vs/base/common/color'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import * as strings from 'vs/base/common/strings'; -import { withNullAsUndefined } from 'vs/base/common/types'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; -import { IPosition } from 'vs/editor/common/core/position'; -import { IRange, Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; import * as languages from 'vs/editor/common/languages'; -import { IModelService } from 'vs/editor/common/services/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import * as nls from 'vs/nls'; -import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMarkdownRendererOptions } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { contrastBorder, editorForeground, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, resolveColorValue, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; -import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; -import { CommentNode } from 'vs/workbench/contrib/comments/browser/commentNode'; +import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply'; import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody'; +import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; +import { CommentNode } from 'vs/workbench/contrib/comments/common/commentModel'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; -import { SimpleCommentEditor } from './simpleCommentEditor'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { IColorTheme } from 'vs/platform/theme/common/themeService'; +import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { Codicon } from 'vs/base/common/codicons'; -import { MarshalledId } from 'vs/base/common/marshallingIds'; - - -const collapseIcon = registerIcon('review-comment-collapse', Codicon.chevronUp, nls.localize('collapseIcon', 'Icon to collapse a review comment.')); +import { IRange } from 'vs/editor/common/core/range'; +import { commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { FontInfo } from 'vs/editor/common/config/fontInfo'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; -const COLLAPSE_ACTION_CLASS = 'expand-review-action ' + ThemeIcon.asClassName(collapseIcon); -const COMMENT_SCHEME = 'comment'; -export function parseMouseDownInfoFromEvent(e: IEditorMouseEvent) { - const range = e.target.range; - - if (!range) { - return null; - } - - if (!e.event.leftButton) { - return null; - } - - if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { - return null; - } - - const data = e.target.detail; - const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; - - // don't collide with folding and git decorations - if (gutterOffsetX > 14) { - return null; - } - - return { lineNumber: range.startLineNumber }; -} - -export function isMouseUpEventMatchMouseDown(mouseDownInfo: { lineNumber: number } | null, e: IEditorMouseEvent) { - if (!mouseDownInfo) { - return null; - } - - const { lineNumber } = mouseDownInfo; - - const range = e.target.range; - - if (!range || range.startLineNumber !== lineNumber) { - return null; - } - - if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { - return null; - } - - return lineNumber; -} - -let INMEM_MODEL_ID = 0; - -export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget { - private _headElement!: HTMLElement; - protected _headingLabel!: HTMLElement; - protected _actionbarWidget!: ActionBar; - private _bodyElement!: HTMLElement; - private _parentEditor: ICodeEditor; - private _commentsElement!: HTMLElement; - private _commentElements: CommentNode[] = []; - private _commentReplyComponent?: { - editor: ICodeEditor; - form: HTMLElement; - commentEditorIsEmpty: IContextKey; - }; - private _reviewThreadReplyButton!: HTMLElement; - private _resizeObserver: any; - private readonly _onDidClose = new Emitter(); - private readonly _onDidCreateThread = new Emitter(); - private _isExpanded?: boolean; - private _collapseAction!: Action; - private _commentGlyph?: CommentGlyphWidget; - private _submitActionsDisposables: IDisposable[]; - private readonly _globalToDispose = new DisposableStore(); +export class CommentThreadWidget extends Disposable implements ICommentThreadWidget { + private _header!: CommentThreadHeader; + private _body!: CommentThreadBody; + private _commentReply?: CommentReply; + private _commentMenus: CommentMenus; private _commentThreadDisposables: IDisposable[] = []; - private _markdownRenderer: MarkdownRenderer; - private _styleElement: HTMLStyleElement; - private _formActions: HTMLElement | null; - private _error!: HTMLElement; - private _contextKeyService: IContextKeyService; private _threadIsEmpty: IContextKey; + private _styleElement: HTMLStyleElement; private _commentThreadContextValue: IContextKey; - private _commentFormActions!: CommentFormActions; - private _scopedInstatiationService: IInstantiationService; - private _focusedComment: number | undefined = undefined; + private _onDidResize = new Emitter(); + onDidResize = this._onDidResize.event; - public get owner(): string { - return this._owner; - } - public get commentThread(): languages.CommentThread { + get commentThread() { return this._commentThread; } - public get extensionId(): string | undefined { - return this._commentThread.extensionId; - } - - private _commentMenus: CommentMenus; - - private _commentOptions: languages.CommentOptions | undefined; - constructor( - editor: ICodeEditor, + readonly container: HTMLElement, private _owner: string, - private _commentThread: languages.CommentThread, + private _parentResourceUri: URI, + private _contextKeyService: IContextKeyService, + private _scopedInstatiationService: IInstantiationService, + private _commentThread: languages.CommentThread, private _pendingComment: string | null, - @IInstantiationService private instantiationService: IInstantiationService, - @ILanguageService private languageService: ILanguageService, - @IModelService private modelService: IModelService, - @IThemeService private themeService: IThemeService, - @ICommentService private commentService: ICommentService, - @IOpenerService private openerService: IOpenerService, - @IContextKeyService contextKeyService: IContextKeyService + private _markdownOptions: IMarkdownRendererOptions, + private _commentOptions: languages.CommentOptions | undefined, + private _containerDelegate: { + actionRunner: (() => void) | null; + collapse: () => void; + }, + @ICommentService private commentService: ICommentService ) { - super(editor, { keepEditorSelection: true }); - this._contextKeyService = contextKeyService.createScoped(this.domNode); - - this._scopedInstatiationService = instantiationService.createChild(new ServiceCollection( - [IContextKeyService, this._contextKeyService] - )); + super(); this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService); this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length); + + this._commentMenus = this.commentService.getCommentMenus(this._owner); + + this._header = new CommentThreadHeader( + container, + { + collapse: this.collapse.bind(this) + }, + this._commentMenus, + this._commentThread, + this._contextKeyService, + this._scopedInstatiationService + ); + + this._header.updateCommentThread(this._commentThread); + + const bodyElement = dom.$('.body'); + container.appendChild(bodyElement); + + this._body = this._scopedInstatiationService.createInstance( + CommentThreadBody, + this._owner, + this._parentResourceUri, + bodyElement, + this._markdownOptions, + this._commentThread, + this._scopedInstatiationService, + this + ) as unknown as CommentThreadBody; + + this._styleElement = dom.createStyleSheet(this.container); + + this._commentThreadContextValue = this._contextKeyService.createKey('commentThread', undefined); this._commentThreadContextValue.set(_commentThread.contextValue); @@ -184,773 +108,199 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (controller) { commentControllerKey.set(controller.contextValue); - this._commentOptions = controller.options; - } - - this._resizeObserver = null; - this._isExpanded = _commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded; - this._commentThreadDisposables = []; - this._submitActionsDisposables = []; - this._formActions = null; - this._commentMenus = this.commentService.getCommentMenus(this._owner); - this.create(); - - this._styleElement = dom.createStyleSheet(this.domNode); - this._globalToDispose.add(this.themeService.onDidColorThemeChange(this._applyTheme, this)); - this._globalToDispose.add(this.editor.onDidChangeConfiguration(e => { - if (e.hasChanged(EditorOption.fontInfo)) { - this._applyTheme(this.themeService.getColorTheme()); - } - })); - this._applyTheme(this.themeService.getColorTheme()); - - this._markdownRenderer = this._globalToDispose.add(new MarkdownRenderer({ editor }, this.languageService, this.openerService)); - this._parentEditor = editor; - } - - public get onDidClose(): Event { - return this._onDidClose.event; - } - - public get onDidCreateThread(): Event { - return this._onDidCreateThread.event; - } - - public getPosition(): IPosition | undefined { - if (this.position) { - return this.position; - } - - if (this._commentGlyph) { - return withNullAsUndefined(this._commentGlyph.getPosition().position); - } - return undefined; - } - - protected override revealLine(lineNumber: number) { - // we don't do anything here as we always do the reveal ourselves. - } - - public reveal(commentUniqueId?: number) { - if (!this._isExpanded) { - this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); - } - - if (commentUniqueId !== undefined) { - let height = this.editor.getLayoutInfo().height; - let matchedNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === commentUniqueId); - if (matchedNode && matchedNode.length) { - const commentThreadCoords = dom.getDomNodePagePosition(this._commentElements[0].domNode); - const commentCoords = dom.getDomNodePagePosition(matchedNode[0].domNode); - - this.editor.setScrollTop(this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top); - return; - } - } - - this.editor.revealRangeInCenter(this._commentThread.range); - } - - public getPendingComment(): string | null { - if (this._commentReplyComponent) { - let model = this._commentReplyComponent.editor.getModel(); - - if (model && model.getValueLength() > 0) { // checking length is cheap - return model.getValue(); - } - } - - return null; - } - - protected _fillContainer(container: HTMLElement): void { - this.setCssClass('review-widget'); - this._headElement = dom.$('.head'); - container.appendChild(this._headElement); - this._fillHead(this._headElement); - - this._bodyElement = dom.$('.body'); - container.appendChild(this._bodyElement); - - dom.addDisposableListener(this._bodyElement, dom.EventType.FOCUS_IN, e => { - this.commentService.setActiveCommentThread(this._commentThread); - }); - } - - protected _fillHead(container: HTMLElement): void { - let titleElement = dom.append(this._headElement, dom.$('.review-title')); - - this._headingLabel = dom.append(titleElement, dom.$('span.filename')); - this.createThreadLabel(); - - const actionsContainer = dom.append(this._headElement, dom.$('.review-actions')); - this._actionbarWidget = new ActionBar(actionsContainer, { - actionViewItemProvider: createActionViewItem.bind(undefined, this.instantiationService) - }); - - this._disposables.add(this._actionbarWidget); - - this._collapseAction = new Action('review.expand', nls.localize('label.collapse', "Collapse"), COLLAPSE_ACTION_CLASS, true, () => this.collapse()); - - const menu = this._commentMenus.getCommentThreadTitleActions(this._commentThread, this._contextKeyService); - this.setActionBarActions(menu); - - this._disposables.add(menu); - this._disposables.add(menu.onDidChange(e => { - this.setActionBarActions(menu); - })); - - this._actionbarWidget.context = this._commentThread; - } - - private setActionBarActions(menu: IMenu): void { - const groups = menu.getActions({ shouldForwardArgs: true }).reduce((r, [, actions]) => [...r, ...actions], <(MenuItemAction | SubmenuItemAction)[]>[]); - this._actionbarWidget.clear(); - this._actionbarWidget.push([...groups, this._collapseAction], { label: false, icon: true }); - } - - private deleteCommentThread(): void { - this.dispose(); - this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); - } - - public collapse(): Promise { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; - if (this._commentThread.comments && this._commentThread.comments.length === 0) { - this.deleteCommentThread(); - return Promise.resolve(); - } - - this.hide(); - return Promise.resolve(); - } - - public getGlyphPosition(): number { - if (this._commentGlyph) { - return this._commentGlyph.getPosition().position!.lineNumber; - } - return 0; - } - - toggleExpand(lineNumber: number) { - if (this._isExpanded) { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; - this.hide(); - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this.deleteCommentThread(); - } - } else { - this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; - this.show({ lineNumber: lineNumber, column: 1 }, 2); } } - async update(commentThread: languages.CommentThread) { - const oldCommentsLen = this._commentElements.length; - const newCommentsLen = commentThread.comments ? commentThread.comments.length : 0; - this._threadIsEmpty.set(!newCommentsLen); - - let commentElementsToDel: CommentNode[] = []; - let commentElementsToDelIndex: number[] = []; - for (let i = 0; i < oldCommentsLen; i++) { - let comment = this._commentElements[i].comment; - let newComment = commentThread.comments ? commentThread.comments.filter(c => c.uniqueIdInThread === comment.uniqueIdInThread) : []; - - if (newComment.length) { - this._commentElements[i].update(newComment[0]); - } else { - commentElementsToDelIndex.push(i); - commentElementsToDel.push(this._commentElements[i]); - } - } - - // del removed elements - for (let i = commentElementsToDel.length - 1; i >= 0; i--) { - this._commentElements.splice(commentElementsToDelIndex[i], 1); - this._commentsElement.removeChild(commentElementsToDel[i].domNode); - } - - let lastCommentElement: HTMLElement | null = null; - let newCommentNodeList: CommentNode[] = []; - let newCommentsInEditMode: CommentNode[] = []; - for (let i = newCommentsLen - 1; i >= 0; i--) { - let currentComment = commentThread.comments![i]; - let oldCommentNode = this._commentElements.filter(commentNode => commentNode.comment.uniqueIdInThread === currentComment.uniqueIdInThread); - if (oldCommentNode.length) { - lastCommentElement = oldCommentNode[0].domNode; - newCommentNodeList.unshift(oldCommentNode[0]); - } else { - const newElement = this.createNewCommentNode(currentComment); - - newCommentNodeList.unshift(newElement); - if (lastCommentElement) { - this._commentsElement.insertBefore(newElement.domNode, lastCommentElement); - lastCommentElement = newElement.domNode; - } else { - this._commentsElement.appendChild(newElement.domNode); - lastCommentElement = newElement.domNode; - } - - if (currentComment.mode === languages.CommentMode.Editing) { - newElement.switchToEditMode(); - newCommentsInEditMode.push(newElement); - } - } + updateCommentThread(commentThread: languages.CommentThread) { + if (this._commentThread !== commentThread) { + this._commentThreadDisposables.forEach(disposable => disposable.dispose()); } this._commentThread = commentThread; - this._commentElements = newCommentNodeList; - this.createThreadLabel(); + this._commentThreadDisposables = []; + this._bindCommentThreadListeners(); - // Move comment glyph widget and show position if the line has changed. - const lineNumber = this._commentThread.range.startLineNumber; - let shouldMoveWidget = false; - if (this._commentGlyph) { - if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { - shouldMoveWidget = true; - this._commentGlyph.setLineNumber(lineNumber); - } - } - - if (!this._reviewThreadReplyButton && this._commentReplyComponent) { - this.createReplyButton(this._commentReplyComponent.editor, this._commentReplyComponent.form); - } - - if (this._commentThread.comments && this._commentThread.comments.length === 0) { - this.expandReplyArea(); - } - - if (shouldMoveWidget && this._isExpanded) { - this.show({ lineNumber, column: 1 }, 2); - } - - if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { - this.show({ lineNumber, column: 1 }, 2); - } else { - this.hide(); - } + this._body.updateCommentThread(commentThread); + this._threadIsEmpty.set(!this._body.length); + this._header.updateCommentThread(commentThread); + this._commentReply?.updateCommentThread(commentThread); if (this._commentThread.contextValue) { this._commentThreadContextValue.set(this._commentThread.contextValue); } else { this._commentThreadContextValue.reset(); } - - if (newCommentsInEditMode.length) { - const lastIndex = this._commentElements.indexOf(newCommentsInEditMode[newCommentsInEditMode.length - 1]); - this._focusedComment = lastIndex; - } - - this.setFocusedComment(this._focusedComment); } - protected override _onWidth(widthInPixel: number): void { - this._commentReplyComponent?.editor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); - } + display(lineHeight: number) { + let headHeight = Math.ceil(lineHeight * 1.2); + this._header.updateHeight(headHeight); - protected override _doLayout(heightInPixel: number, widthInPixel: number): void { - this._commentReplyComponent?.editor.layout({ height: 5 * 18, width: widthInPixel - 54 /* margin 20px * 10 + scrollbar 14px*/ }); - } - - display(lineNumber: number) { - this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); - - this._disposables.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); - this._disposables.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); - - let headHeight = Math.ceil(this.editor.getOption(EditorOption.lineHeight) * 1.2); - this._headElement.style.height = `${headHeight}px`; - this._headElement.style.lineHeight = this._headElement.style.height; - - this._commentsElement = dom.append(this._bodyElement, dom.$('div.comments-container')); - this._commentsElement.setAttribute('role', 'presentation'); - this._commentsElement.tabIndex = 0; - - this._disposables.add(dom.addDisposableListener(this._commentsElement, dom.EventType.KEY_DOWN, (e) => { - let event = new StandardKeyboardEvent(e as KeyboardEvent); - if (event.equals(KeyCode.UpArrow) || event.equals(KeyCode.DownArrow)) { - const moveFocusWithinBounds = (change: number): number => { - if (this._focusedComment === undefined && change >= 0) { return 0; } - if (this._focusedComment === undefined && change < 0) { return this._commentElements.length - 1; } - let newIndex = this._focusedComment! + change; - return Math.min(Math.max(0, newIndex), this._commentElements.length - 1); - }; - - this.setFocusedComment(event.equals(KeyCode.UpArrow) ? moveFocusWithinBounds(-1) : moveFocusWithinBounds(1)); - } - })); - - this._commentElements = []; - if (this._commentThread.comments) { - for (const comment of this._commentThread.comments) { - const newCommentNode = this.createNewCommentNode(comment); - - this._commentElements.push(newCommentNode); - this._commentsElement.appendChild(newCommentNode.domNode); - if (comment.mode === languages.CommentMode.Editing) { - newCommentNode.switchToEditMode(); - } - } - } + this._body.display(); // create comment thread only when it supports reply if (this._commentThread.canReply) { - this.createCommentForm(); + this._createCommentForm(); } - this._resizeObserver = new MutationObserver(this._refresh.bind(this)); - - this._resizeObserver.observe(this._bodyElement, { - attributes: true, - childList: true, - characterData: true, - subtree: true - }); - - if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { - this.show({ lineNumber: lineNumber, column: 1 }, 2); - } + this._register(this._body.onDidResize(dimension => { + this._refresh(dimension); + })); // If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus. // if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately. - if (this._commentThread.canReply && this._commentReplyComponent) { - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this._commentReplyComponent.editor.focus(); - } else if (this._commentReplyComponent.editor.getModel()!.getValueLength() > 0) { - this.expandReplyArea(); - } + if (this._commentThread.canReply && this._commentReply) { + this._commentReply?.focusIfNeeded(); } + this._bindCommentThreadListeners(); + } + + private _refresh(dimension: dom.Dimension) { + this._body.layout(); + this._onDidResize.fire(dimension); + } + + + + private _bindCommentThreadListeners() { this._commentThreadDisposables.push(this._commentThread.onDidChangeCanReply(() => { - if (this._commentReplyComponent) { - if (!this._commentThread.canReply) { - this._commentReplyComponent.form.style.display = 'none'; - } else { - this._commentReplyComponent.form.style.display = 'block'; - } + if (this._commentReply) { + this._commentReply.updateCanReply(); } else { if (this._commentThread.canReply) { - this.createCommentForm(); - } - } - })); - } - - private createCommentForm() { - const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; - const commentForm = dom.append(this._bodyElement, dom.$('.comment-form')); - const commentEditor = this._scopedInstatiationService.createInstance(SimpleCommentEditor, commentForm, SimpleCommentEditor.getEditorOptions(), this._parentEditor, this); - const commentEditorIsEmpty = CommentContextKeys.commentIsEmpty.bindTo(this._contextKeyService); - commentEditorIsEmpty.set(!this._pendingComment); - - this._commentReplyComponent = { - form: commentForm, - editor: commentEditor, - commentEditorIsEmpty - }; - - const modeId = generateUuid() + '-' + (hasExistingComments ? this._commentThread.threadId : ++INMEM_MODEL_ID); - const params = JSON.stringify({ - extensionId: this.extensionId, - commentThreadId: this.commentThread.threadId - }); - - let resource = URI.parse(`${COMMENT_SCHEME}://${this.extensionId}/commentinput-${modeId}.md?${params}`); // TODO. Remove params once extensions adopt authority. - let commentController = this.commentService.getCommentController(this.owner); - if (commentController) { - resource = resource.with({ authority: commentController.id }); - } - - const model = this.modelService.createModel(this._pendingComment || '', this.languageService.createByFilepathOrFirstLine(resource), resource, false); - this._disposables.add(model); - commentEditor.setModel(model); - this._disposables.add(commentEditor); - this._disposables.add(commentEditor.getModel()!.onDidChangeContent(() => { - this.setCommentEditorDecorations(); - commentEditorIsEmpty?.set(!commentEditor.getValue()); - })); - - this.createTextModelListener(commentEditor, commentForm); - - this.setCommentEditorDecorations(); - - // Only add the additional step of clicking a reply button to expand the textarea when there are existing comments - if (hasExistingComments) { - this.createReplyButton(commentEditor, commentForm); - } else { - if (this._commentThread.comments && this._commentThread.comments.length === 0) { - this.expandReplyArea(); - } - } - this._error = dom.append(commentForm, dom.$('.validation-error.hidden')); - - this._formActions = dom.append(commentForm, dom.$('.form-actions')); - this.createCommentWidgetActions(this._formActions, model); - this.createCommentWidgetActionsListener(); - } - - private createTextModelListener(commentEditor: ICodeEditor, commentForm: HTMLElement) { - this._commentThreadDisposables.push(commentEditor.onDidFocusEditorWidget(() => { - this._commentThread.input = { - uri: commentEditor.getModel()!.uri, - value: commentEditor.getValue() - }; - this.commentService.setActiveCommentThread(this._commentThread); - })); - - this._commentThreadDisposables.push(commentEditor.getModel()!.onDidChangeContent(() => { - let modelContent = commentEditor.getValue(); - if (this._commentThread.input && this._commentThread.input.uri === commentEditor.getModel()!.uri && this._commentThread.input.value !== modelContent) { - let newInput: languages.CommentInput = this._commentThread.input; - newInput.value = modelContent; - this._commentThread.input = newInput; - } - this.commentService.setActiveCommentThread(this._commentThread); - })); - - this._commentThreadDisposables.push(this._commentThread.onDidChangeInput(input => { - let thread = this._commentThread; - - if (thread.input && thread.input.uri !== commentEditor.getModel()!.uri) { - return; - } - if (!input) { - return; - } - - if (commentEditor.getValue() !== input.value) { - commentEditor.setValue(input.value); - - if (input.value === '') { - this._pendingComment = ''; - commentForm.classList.remove('expand'); - commentEditor.getDomNode()!.style.outline = ''; - this._error.textContent = ''; - this._error.classList.add('hidden'); + this._createCommentForm(); } } })); this._commentThreadDisposables.push(this._commentThread.onDidChangeComments(async _ => { - await this.update(this._commentThread); + await this.updateCommentThread(this._commentThread); })); this._commentThreadDisposables.push(this._commentThread.onDidChangeLabel(_ => { - this.createThreadLabel(); + this._header.createThreadLabel(); })); } - private createCommentWidgetActionsListener() { - this._commentThreadDisposables.push(this._commentThread.onDidChangeRange(range => { - // Move comment glyph widget and show position if the line has changed. - const lineNumber = this._commentThread.range.startLineNumber; - let shouldMoveWidget = false; - if (this._commentGlyph) { - if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { - shouldMoveWidget = true; - this._commentGlyph.setLineNumber(lineNumber); - } - } - - if (shouldMoveWidget && this._isExpanded) { - this.show({ lineNumber, column: 1 }, 2); - } - })); - - this._commentThreadDisposables.push(this._commentThread.onDidChangeCollasibleState(state => { - if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { - const lineNumber = this._commentThread.range.startLineNumber; - - this.show({ lineNumber, column: 1 }, 2); - return; - } - - if (state === languages.CommentThreadCollapsibleState.Collapsed && this._isExpanded) { - this.hide(); - return; - } - })); - } - - private setFocusedComment(value: number | undefined) { - if (this._focusedComment !== undefined) { - this._commentElements[this._focusedComment]?.setFocus(false); - } - - if (this._commentElements.length === 0 || value === undefined) { - this._focusedComment = undefined; - } else { - this._focusedComment = Math.min(value, this._commentElements.length - 1); - this._commentElements[this._focusedComment].setFocus(true); - } - } - - private getActiveComment(): CommentNode | ReviewZoneWidget { - return this._commentElements.filter(node => node.isEditing)[0] || this; - } - - /** - * Command based actions. - */ - private createCommentWidgetActions(container: HTMLElement, model: ITextModel) { - const commentThread = this._commentThread; - - const menu = this._commentMenus.getCommentThreadActions(commentThread, this._contextKeyService); - - this._disposables.add(menu); - this._disposables.add(menu.onDidChange(() => { - this._commentFormActions.setActions(menu); - })); - - this._commentFormActions = new CommentFormActions(container, async (action: IAction) => { - if (!commentThread.comments || !commentThread.comments.length) { - let newPosition = this.getPosition(); - - if (newPosition) { - this.commentService.updateCommentThreadTemplate(this.owner, commentThread.commentThreadHandle, new Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1)); - } - } - action.run({ - thread: this._commentThread, - text: this._commentReplyComponent?.editor.getValue(), - $mid: MarshalledId.CommentThreadReply - }); - - this.hideReplyArea(); - }, this.themeService); - - this._commentFormActions.setActions(menu); - } - - private createNewCommentNode(comment: languages.Comment): CommentNode { - let newCommentNode = this._scopedInstatiationService.createInstance(CommentNode, + private _createCommentForm() { + this._commentReply = this._scopedInstatiationService.createInstance( + CommentReply, + this._owner, + this._body.container, this._commentThread, - comment, - this.owner, - this.editor.getModel()!.uri, - this._parentEditor, + this._scopedInstatiationService, + this._contextKeyService, + this._commentMenus, + this._commentOptions, + this._pendingComment, this, - this._markdownRenderer); + this._containerDelegate.actionRunner + ); - this._disposables.add(newCommentNode); - this._disposables.add(newCommentNode.onDidClick(clickedNode => - this.setFocusedComment(this._commentElements.findIndex(commentNode => commentNode.comment.uniqueIdInThread === clickedNode.comment.uniqueIdInThread)) - )); - - return newCommentNode; + this._register(this._commentReply); } - async submitComment(): Promise { - const activeComment = this.getActiveComment(); - if (activeComment instanceof ReviewZoneWidget) { - if (this._commentFormActions) { - this._commentFormActions.triggerDefaultAction(); - } + getCommentCoords(commentUniqueId: number) { + return this._body.getCommentCoords(commentUniqueId); + } + + getPendingComment(): string | null { + if (this._commentReply) { + return this._commentReply.getPendingComment(); + } + + return null; + } + + getDimensions() { + return this._body?.getDimensions(); + } + + layout(widthInPixel?: number) { + this._body.layout(); + + if (widthInPixel !== undefined) { + this._commentReply?.layout(widthInPixel); } } - private createThreadLabel() { - let label: string | undefined; - label = this._commentThread.label; + focusCommentEditor() { + this._commentReply?.focusCommentEditor(); + } - if (label === undefined) { - if (!(this._commentThread.comments && this._commentThread.comments.length)) { - label = nls.localize('startThread', "Start discussion"); - } - } + focus() { + this._body.focus(); + } - if (label) { - this._headingLabel.textContent = strings.escape(label); - this._headingLabel.setAttribute('aria-label', label); + async submitComment() { + const activeComment = this._body.activeComment; + if (activeComment && !(activeComment instanceof CommentNode)) { + this._commentReply?.submitComment(); } } - private expandReplyArea() { - if (!this._commentReplyComponent?.form.classList.contains('expand')) { - this._commentReplyComponent?.form.classList.add('expand'); - this._commentReplyComponent?.editor.focus(); - this._commentReplyComponent?.editor.layout(); - } + collapse() { + this._containerDelegate.collapse(); } - private hideReplyArea() { - if (this._commentReplyComponent) { - this._commentReplyComponent.editor.setValue(''); - this._commentReplyComponent.editor.getDomNode()!.style.outline = ''; - } - this._pendingComment = ''; - this._commentReplyComponent?.form.classList.remove('expand'); - this._error.textContent = ''; - this._error.classList.add('hidden'); - } - - private createReplyButton(commentEditor: ICodeEditor, commentForm: HTMLElement) { - this._reviewThreadReplyButton = dom.append(commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); - this._reviewThreadReplyButton.title = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); - - this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); - // bind click/escape actions for reviewThreadReplyButton and textArea - this._disposables.add(dom.addDisposableListener(this._reviewThreadReplyButton, 'click', _ => this.expandReplyArea())); - this._disposables.add(dom.addDisposableListener(this._reviewThreadReplyButton, 'focus', _ => this.expandReplyArea())); - - commentEditor.onDidBlurEditorWidget(() => { - if (commentEditor.getModel()!.getValueLength() === 0 && commentForm.classList.contains('expand')) { - commentForm.classList.remove('expand'); - } - }); - } - - _refresh() { - if (this._isExpanded && this._bodyElement) { - let dimensions = dom.getClientArea(this._bodyElement); - - this._commentElements.forEach(element => { - element.layout(); - }); - - const headHeight = Math.ceil(this.editor.getOption(EditorOption.lineHeight) * 1.2); - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - const arrowHeight = Math.round(lineHeight / 3); - const frameThickness = Math.round(lineHeight / 9) * 2; - - const computedLinesNumber = Math.ceil((headHeight + dimensions.height + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */) / lineHeight); - - if (this._viewZone?.heightInLines === computedLinesNumber) { - return; - } - - let currentPosition = this.getPosition(); - - if (this._viewZone && currentPosition && currentPosition.lineNumber !== this._viewZone.afterLineNumber) { - this._viewZone.afterLineNumber = currentPosition.lineNumber; - } - - if (!this._commentThread.comments || !this._commentThread.comments.length) { - this._commentReplyComponent?.editor.focus(); - } - - this._relayout(computedLinesNumber); - } - } - - private setCommentEditorDecorations() { - const model = this._commentReplyComponent && this._commentReplyComponent.editor.getModel(); - if (model) { - const valueLength = model.getValueLength(); - const hasExistingComments = this._commentThread.comments && this._commentThread.comments.length > 0; - const placeholder = valueLength > 0 - ? '' - : hasExistingComments - ? (this._commentOptions?.placeHolder || nls.localize('reply', "Reply...")) - : (this._commentOptions?.placeHolder || nls.localize('newComment', "Type a new comment")); - const decorations = [{ - range: { - startLineNumber: 0, - endLineNumber: 0, - startColumn: 0, - endColumn: 1 - }, - renderOptions: { - after: { - contentText: placeholder, - color: `${resolveColorValue(editorForeground, this.themeService.getColorTheme())?.transparent(0.4)}` - } - } - }]; - - this._commentReplyComponent?.editor.setDecorations('review-zone-widget', COMMENTEDITOR_DECORATION_KEY, decorations); - } - } - - private mouseDownInfo: { lineNumber: number } | null = null; - - private onEditorMouseDown(e: IEditorMouseEvent): void { - this.mouseDownInfo = parseMouseDownInfoFromEvent(e); - } - - private onEditorMouseUp(e: IEditorMouseEvent): void { - const matchedLineNumber = isMouseUpEventMatchMouseDown(this.mouseDownInfo, e); - this.mouseDownInfo = null; - - if (matchedLineNumber === null || !e.target.element) { - return; - } - - if (this._commentGlyph && this._commentGlyph.getPosition().position!.lineNumber !== matchedLineNumber) { - return; - } - - if (e.target.element.className.indexOf('comment-thread') >= 0) { - this.toggleExpand(matchedLineNumber); - } - } - - private _applyTheme(theme: IColorTheme) { - const borderColor = theme.getColor(peekViewBorder); - this.style({ - arrowColor: borderColor || Color.transparent, - frameColor: borderColor || Color.transparent - }); - + applyTheme(theme: IColorTheme, fontInfo: FontInfo) { const content: string[] = []; - if (borderColor) { - content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid ${borderColor} }`); - } + content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`); const linkColor = theme.getColor(textLinkForeground); if (linkColor) { - content.push(`.monaco-editor .review-widget .body .comment-body a { color: ${linkColor} }`); + content.push(`.review-widget .body .comment-body a { color: ${linkColor} }`); } const linkActiveColor = theme.getColor(textLinkActiveForeground); if (linkActiveColor) { - content.push(`.monaco-editor .review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`); + content.push(`.review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`); } const focusColor = theme.getColor(focusBorder); if (focusColor) { - content.push(`.monaco-editor .review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`); - content.push(`.monaco-editor .review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); + content.push(`.review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`); + content.push(`.review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); } const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); if (blockQuoteBackground) { - content.push(`.monaco-editor .review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`); + content.push(`.review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`); } const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder); if (blockQuoteBOrder) { - content.push(`.monaco-editor .review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`); + content.push(`.review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`); } const border = theme.getColor(PANEL_BORDER); if (border) { - content.push(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`); + content.push(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`); } const hcBorder = theme.getColor(contrastBorder); if (hcBorder) { - content.push(`.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); - content.push(`.monaco-editor .review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`); + content.push(`.review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); + content.push(`.review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`); } const errorBorder = theme.getColor(inputValidationErrorBorder); if (errorBorder) { - content.push(`.monaco-editor .review-widget .validation-error { border: 1px solid ${errorBorder}; }`); + content.push(`.review-widget .validation-error { border: 1px solid ${errorBorder}; }`); } const errorBackground = theme.getColor(inputValidationErrorBackground); if (errorBackground) { - content.push(`.monaco-editor .review-widget .validation-error { background: ${errorBackground}; }`); + content.push(`.review-widget .validation-error { background: ${errorBackground}; }`); } const errorForeground = theme.getColor(inputValidationErrorForeground); if (errorForeground) { - content.push(`.monaco-editor .review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`); + content.push(`.review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`); } - const fontInfo = this.editor.getOption(EditorOption.fontInfo); const fontFamilyVar = '--comment-thread-editor-font-family'; const fontSizeVar = '--comment-thread-editor-font-size'; const fontWeightVar = '--comment-thread-editor-font-weight'; @@ -958,50 +308,13 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this.container?.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`); this.container?.style.setProperty(fontWeightVar, fontInfo.fontWeight); - content.push(`.monaco-editor .review-widget .body code { + content.push(`.review-widget .body code { font-family: var(${fontFamilyVar}); font-weight: var(${fontWeightVar}); font-size: var(${fontSizeVar}); }`); this._styleElement.textContent = content.join('\n'); - - // Editor decorations should also be responsive to theme changes - this.setCommentEditorDecorations(); - } - - override show(rangeOrPos: IRange | IPosition, heightInLines: number): void { - this._isExpanded = true; - super.show(rangeOrPos, heightInLines); - this._refresh(); - } - - override hide() { - if (this._isExpanded) { - this._isExpanded = false; - // Focus the container so that the comment editor will be blurred before it is hidden - if (this.editor.hasWidgetFocus()) { - this.editor.focus(); - } - } - super.hide(); - } - - override dispose() { - super.dispose(); - if (this._resizeObserver) { - this._resizeObserver.disconnect(); - this._resizeObserver = null; - } - - if (this._commentGlyph) { - this._commentGlyph.dispose(); - this._commentGlyph = undefined; - } - - this._globalToDispose.dispose(); - this._commentThreadDisposables.forEach(global => global.dispose()); - this._submitActionsDisposables.forEach(local => local.dispose()); - this._onDidClose.fire(undefined); + this._commentReply?.setCommentEditorDecorations(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts new file mode 100644 index 00000000000..151a49ffbb6 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -0,0 +1,453 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { Color } from 'vs/base/common/color'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import * as languages from 'vs/editor/common/languages'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { CommentGlyphWidget } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { CommentThreadWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { commentThreadStateColorVar, getCommentThreadStateColor } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { peekViewBorder } from 'vs/editor/contrib/peekView/browser/peekView'; + +export function getCommentThreadWidgetStateColor(thread: languages.CommentThread, theme: IColorTheme): Color | undefined { + return getCommentThreadStateColor(thread, theme) ?? theme.getColor(peekViewBorder); +} + +export function parseMouseDownInfoFromEvent(e: IEditorMouseEvent) { + const range = e.target.range; + + if (!range) { + return null; + } + + if (!e.event.leftButton) { + return null; + } + + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + return null; + } + + const data = e.target.detail; + const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft; + + // don't collide with folding and git decorations + if (gutterOffsetX > 14) { + return null; + } + + return { lineNumber: range.startLineNumber }; +} + +export function isMouseUpEventMatchMouseDown(mouseDownInfo: { lineNumber: number } | null, e: IEditorMouseEvent) { + if (!mouseDownInfo) { + return null; + } + + const { lineNumber } = mouseDownInfo; + + const range = e.target.range; + + if (!range || range.startLineNumber !== lineNumber) { + return null; + } + + if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) { + return null; + } + + return lineNumber; +} + +export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget { + private _commentThreadWidget!: CommentThreadWidget; + private readonly _onDidClose = new Emitter(); + private readonly _onDidCreateThread = new Emitter(); + private _isExpanded?: boolean; + private _commentGlyph?: CommentGlyphWidget; + private readonly _globalToDispose = new DisposableStore(); + private _commentThreadDisposables: IDisposable[] = []; + private _contextKeyService: IContextKeyService; + private _scopedInstatiationService: IInstantiationService; + + public get owner(): string { + return this._owner; + } + public get commentThread(): languages.CommentThread { + return this._commentThread; + } + + private _commentOptions: languages.CommentOptions | undefined; + + constructor( + editor: ICodeEditor, + private _owner: string, + private _commentThread: languages.CommentThread, + private _pendingComment: string | null, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService private themeService: IThemeService, + @ICommentService private commentService: ICommentService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(editor, { keepEditorSelection: true }); + this._contextKeyService = contextKeyService.createScoped(this.domNode); + + this._scopedInstatiationService = instantiationService.createChild(new ServiceCollection( + [IContextKeyService, this._contextKeyService] + )); + + const controller = this.commentService.getCommentController(this._owner); + if (controller) { + this._commentOptions = controller.options; + } + + this._isExpanded = _commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded; + this._commentThreadDisposables = []; + this.create(); + + this._globalToDispose.add(this.themeService.onDidColorThemeChange(this._applyTheme, this)); + this._globalToDispose.add(this.editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.fontInfo)) { + this._applyTheme(this.themeService.getColorTheme()); + } + })); + this._applyTheme(this.themeService.getColorTheme()); + + } + + public get onDidClose(): Event { + return this._onDidClose.event; + } + + public get onDidCreateThread(): Event { + return this._onDidCreateThread.event; + } + + public getPosition(): IPosition | undefined { + if (this.position) { + return this.position; + } + + if (this._commentGlyph) { + return withNullAsUndefined(this._commentGlyph.getPosition().position); + } + return undefined; + } + + protected override revealLine(lineNumber: number) { + // we don't do anything here as we always do the reveal ourselves. + } + + public reveal(commentUniqueId?: number, focus: boolean = false) { + if (!this._isExpanded) { + this.show({ lineNumber: this._commentThread.range.startLineNumber, column: 1 }, 2); + } + + if (commentUniqueId !== undefined) { + let height = this.editor.getLayoutInfo().height; + const coords = this._commentThreadWidget.getCommentCoords(commentUniqueId); + if (coords) { + const commentThreadCoords = coords.thread; + const commentCoords = coords.comment; + + this.editor.setScrollTop(this.editor.getTopForLineNumber(this._commentThread.range.startLineNumber) - height / 2 + commentCoords.top - commentThreadCoords.top); + return; + } + } + + this.editor.revealRangeInCenter(this._commentThread.range); + if (focus) { + this._commentThreadWidget.focus(); + } + } + + public getPendingComment(): string | null { + return this._commentThreadWidget.getPendingComment(); + } + + protected _fillContainer(container: HTMLElement): void { + this.setCssClass('review-widget'); + this._commentThreadWidget = this._scopedInstatiationService.createInstance( + CommentThreadWidget, + container, + this._owner, + this.editor.getModel()!.uri, + this._contextKeyService, + this._scopedInstatiationService, + this._commentThread as unknown as languages.CommentThread, + this._pendingComment, + { editor: this.editor }, + this._commentOptions, + { + actionRunner: () => { + if (!this._commentThread.comments || !this._commentThread.comments.length) { + let newPosition = this.getPosition(); + + if (newPosition) { + this.commentService.updateCommentThreadTemplate(this.owner, this._commentThread.commentThreadHandle, new Range(newPosition.lineNumber, 1, newPosition.lineNumber, 1)); + } + } + }, + collapse: () => { + this.collapse(); + } + } + ) as unknown as CommentThreadWidget; + + this._disposables.add(this._commentThreadWidget); + } + + private deleteCommentThread(): void { + this.dispose(); + this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId); + } + + public collapse(): Promise { + this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; + if (this._commentThread.comments && this._commentThread.comments.length === 0) { + this.deleteCommentThread(); + return Promise.resolve(); + } + + this.hide(); + return Promise.resolve(); + } + + public getGlyphPosition(): number { + if (this._commentGlyph) { + return this._commentGlyph.getPosition().position!.lineNumber; + } + return 0; + } + + toggleExpand(lineNumber: number) { + if (this._isExpanded) { + this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Collapsed; + this.hide(); + if (!this._commentThread.comments || !this._commentThread.comments.length) { + this.deleteCommentThread(); + } + } else { + this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; + this.show({ lineNumber: lineNumber, column: 1 }, 2); + } + } + + async update(commentThread: languages.CommentThread) { + if (this._commentThread !== commentThread) { + this._commentThreadDisposables.forEach(disposable => disposable.dispose()); + this._commentThread = commentThread; + this._commentThreadDisposables = []; + this.bindCommentThreadListeners(); + } + + this._commentThreadWidget.updateCommentThread(commentThread); + + // Move comment glyph widget and show position if the line has changed. + const lineNumber = this._commentThread.range.startLineNumber; + let shouldMoveWidget = false; + if (this._commentGlyph) { + if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { + shouldMoveWidget = true; + this._commentGlyph.setLineNumber(lineNumber); + } + } + + if (shouldMoveWidget && this._isExpanded) { + this.show({ lineNumber, column: 1 }, 2); + } + + if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { + this.show({ lineNumber, column: 1 }, 2); + } else { + this.hide(); + } + } + + protected override _onWidth(widthInPixel: number): void { + this._commentThreadWidget.layout(widthInPixel); + } + + protected override _doLayout(heightInPixel: number, widthInPixel: number): void { + this._commentThreadWidget.layout(widthInPixel); + } + + display(lineNumber: number) { + this._commentGlyph = new CommentGlyphWidget(this.editor, lineNumber); + + this._disposables.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); + this._disposables.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); + + this._commentThreadWidget.display(this.editor.getOption(EditorOption.lineHeight)); + this._disposables.add(this._commentThreadWidget.onDidResize(dimension => { + this._refresh(dimension); + })); + if (this._commentThread.collapsibleState === languages.CommentThreadCollapsibleState.Expanded) { + this.show({ lineNumber: lineNumber, column: 1 }, 2); + } + + this.bindCommentThreadListeners(); + } + + private bindCommentThreadListeners() { + this._commentThreadDisposables.push(this._commentThread.onDidChangeComments(async _ => { + await this.update(this._commentThread); + })); + + this._commentThreadDisposables.push(this._commentThread.onDidChangeRange(range => { + // Move comment glyph widget and show position if the line has changed. + const lineNumber = this._commentThread.range.startLineNumber; + let shouldMoveWidget = false; + if (this._commentGlyph) { + if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { + shouldMoveWidget = true; + this._commentGlyph.setLineNumber(lineNumber); + } + } + + if (shouldMoveWidget && this._isExpanded) { + this.show({ lineNumber, column: 1 }, 2); + } + })); + + this._commentThreadDisposables.push(this._commentThread.onDidChangeCollasibleState(state => { + if (state === languages.CommentThreadCollapsibleState.Expanded && !this._isExpanded) { + const lineNumber = this._commentThread.range.startLineNumber; + + this.show({ lineNumber, column: 1 }, 2); + return; + } + + if (state === languages.CommentThreadCollapsibleState.Collapsed && this._isExpanded) { + this.hide(); + return; + } + })); + + + this._commentThreadDisposables.push(this._commentThread.onDidChangeState(() => { + const borderColor = + getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + this.style({ + frameColor: borderColor, + arrowColor: borderColor, + }); + this.container?.style.setProperty(commentThreadStateColorVar, `${borderColor}`); + })); + } + + async submitComment(): Promise { + this._commentThreadWidget.submitComment(); + } + + _refresh(dimensions?: dom.Dimension) { + if (this._isExpanded && dimensions) { + this._commentThreadWidget.layout(); + + const headHeight = Math.ceil(this.editor.getOption(EditorOption.lineHeight) * 1.2); + const lineHeight = this.editor.getOption(EditorOption.lineHeight); + const arrowHeight = Math.round(lineHeight / 3); + const frameThickness = Math.round(lineHeight / 9) * 2; + + const computedLinesNumber = Math.ceil((headHeight + dimensions.height + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */) / lineHeight); + + if (this._viewZone?.heightInLines === computedLinesNumber) { + return; + } + + let currentPosition = this.getPosition(); + + if (this._viewZone && currentPosition && currentPosition.lineNumber !== this._viewZone.afterLineNumber) { + this._viewZone.afterLineNumber = currentPosition.lineNumber; + } + + if (!this._commentThread.comments || !this._commentThread.comments.length) { + this._commentThreadWidget.focusCommentEditor(); + } + + this._relayout(computedLinesNumber); + } + } + + private mouseDownInfo: { lineNumber: number } | null = null; + + private onEditorMouseDown(e: IEditorMouseEvent): void { + this.mouseDownInfo = parseMouseDownInfoFromEvent(e); + } + + private onEditorMouseUp(e: IEditorMouseEvent): void { + const matchedLineNumber = isMouseUpEventMatchMouseDown(this.mouseDownInfo, e); + this.mouseDownInfo = null; + + if (matchedLineNumber === null || !e.target.element) { + return; + } + + if (this._commentGlyph && this._commentGlyph.getPosition().position!.lineNumber !== matchedLineNumber) { + return; + } + + if (e.target.element.className.indexOf('comment-thread') >= 0) { + this.toggleExpand(matchedLineNumber); + } + } + + private _applyTheme(theme: IColorTheme) { + const borderColor = getCommentThreadWidgetStateColor(this._commentThread, this.themeService.getColorTheme()) || Color.transparent; + this.style({ + arrowColor: borderColor, + frameColor: borderColor + }); + const fontInfo = this.editor.getOption(EditorOption.fontInfo); + + // Editor decorations should also be responsive to theme changes + this._commentThreadWidget.applyTheme(theme, fontInfo); + } + + override show(rangeOrPos: IRange | IPosition, heightInLines: number): void { + this._isExpanded = true; + super.show(rangeOrPos, heightInLines); + this._refresh(this._commentThreadWidget.getDimensions()); + } + + override hide() { + if (this._isExpanded) { + this._isExpanded = false; + // Focus the container so that the comment editor will be blurred before it is hidden + if (this.editor.hasWidgetFocus()) { + this.editor.focus(); + } + } + super.hide(); + } + + override dispose() { + super.dispose(); + + if (this._commentGlyph) { + this._commentGlyph.dispose(); + this._commentGlyph = undefined; + } + + this._globalToDispose.dispose(); + this._commentThreadDisposables.forEach(global => global.dispose()); + this._onDidClose.fire(undefined); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index f2c6b1d3b4b..0d515f2ae79 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -10,10 +10,6 @@ import 'vs/workbench/contrib/comments/browser/commentsEditorContribution'; import { ICommentService, CommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -export interface ICommentsConfiguration { - openPanel: 'neverOpen' | 'openOnSessionStart' | 'openOnSessionStartWithComments'; -} - Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ id: 'comments', order: 20, @@ -24,13 +20,20 @@ Registry.as(ConfigurationExtensions.Configuration).regis enum: ['neverOpen', 'openOnSessionStart', 'openOnSessionStartWithComments'], default: 'openOnSessionStartWithComments', description: nls.localize('openComments', "Controls when the comments panel should open."), + restricted: false, + markdownDeprecationMessage: nls.localize('comments.openPanel.deprecated', "This setting is deprecated in favor of `comments.openView`.") + }, + 'comments.openView': { + enum: ['never', 'file'], + enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active.")], + default: 'file', + description: nls.localize('comments.openView', "Controls when the comments view should open."), restricted: false }, 'comments.useRelativeTime': { type: 'boolean', default: true, description: nls.localize('useRelativeTime', "Determines if relative time will be used in comment timestamps (ex. '1 day ago').") - } } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index e8abd1ef104..7c6e0a160ef 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -9,14 +9,14 @@ import { coalesce, findFirstInSorted } from 'vs/base/common/arrays'; import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/review'; import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, isCodeEditor, isDiffEditor, IViewZone } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange, Range } from 'vs/editor/common/core/range'; import { IEditorContribution, IModelChangedEvent } from 'vs/editor/common/editorCommon'; -import { IModelDecorationOptions } from 'vs/editor/common/model'; +import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import * as languages from 'vs/editor/common/languages'; import { peekViewResultsBackground, peekViewResultsSelectionBackground, peekViewTitleBackground } from 'vs/editor/contrib/peekView/browser/peekView'; @@ -29,13 +29,23 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND } from 'vs/workbench/common/theme'; -import { overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; +import { CommentGlyphWidget, overviewRulerCommentingRangeForeground } from 'vs/workbench/contrib/comments/browser/commentGlyphWidget'; import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; -import { COMMENTEDITOR_DECORATION_KEY, isMouseUpEventMatchMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; +import { isMouseUpEventMatchMouseDown, parseMouseDownInfoFromEvent, ReviewZoneWidget } from 'vs/workbench/contrib/comments/browser/commentThreadZoneWidget'; import { ctxCommentEditorFocused, SimpleCommentEditor } from 'vs/workbench/contrib/comments/browser/simpleCommentEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IViewsService } from 'vs/workbench/common/views'; +import { COMMENTS_VIEW_ID } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; +import { COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentReply'; +import { Emitter } from 'vs/base/common/event'; +import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Position } from 'vs/editor/common/core/position'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; export const ID = 'editor.contrib.review'; @@ -56,25 +66,29 @@ export class ReviewViewZone implements IViewZone { } } -class CommentingRangeDecoration { - private _decorationId: string; +class CommentingRangeDecoration implements IModelDeltaDecoration { + private _decorationId: string | undefined; + private _startLineNumber: number; + private _endLineNumber: number; - public get id(): string { + public get id(): string | undefined { return this._decorationId; } - constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, commentingOptions: ModelDecorationOptions, private commentingRangesInfo: languages.CommentingRanges) { - const startLineNumber = _range.startLineNumber; - const endLineNumber = _range.endLineNumber; - let commentingRangeDecorations = [{ - range: { - startLineNumber: startLineNumber, startColumn: 1, - endLineNumber: endLineNumber, endColumn: 1 - }, - options: commentingOptions - }]; + public set id(id: string | undefined) { + this._decorationId = id; + } - this._decorationId = this._editor.deltaDecorations([], commentingRangeDecorations)[0]; + public get range(): IRange { + return { + startLineNumber: this._startLineNumber, startColumn: 1, + endLineNumber: this._endLineNumber, endColumn: 1 + }; + } + + constructor(private _editor: ICodeEditor, private _ownerId: string, private _extensionId: string | undefined, private _label: string | undefined, private _range: IRange, public readonly options: ModelDecorationOptions, private commentingRangesInfo: languages.CommentingRanges, public readonly isHover: boolean = false) { + this._startLineNumber = _range.startLineNumber; + this._endLineNumber = _range.endLineNumber; } public getCommentAction(): { ownerId: string; extensionId: string | undefined; label: string | undefined; commentingRangesInfo: languages.CommentingRanges } { @@ -91,25 +105,54 @@ class CommentingRangeDecoration { } public getActiveRange() { - return this._editor.getModel()!.getDecorationRange(this._decorationId); + return this.id ? this._editor.getModel()!.getDecorationRange(this.id) : undefined; } } -class CommentingRangeDecorator { - private decorationOptions: ModelDecorationOptions; +class CommentingRangeDecorator { + public static description = 'commenting-range-decorator'; + private decorationOptions!: ModelDecorationOptions; + private hoverDecorationOptions!: ModelDecorationOptions; private commentingRangeDecorations: CommentingRangeDecoration[] = []; + private decorationIds: string[] = []; + private _editor: ICodeEditor | undefined; + private _infos: ICommentInfo[] | undefined; + private _lastHover: number = -1; + private _onDidChangeDecorationsCount: Emitter = new Emitter(); + public readonly onDidChangeDecorationsCount = this._onDidChangeDecorationsCount.event; constructor() { const decorationOptions: IModelDecorationOptions = { - description: 'commenting-range-decorator', + description: CommentingRangeDecorator.description, isWholeLine: true, linesDecorationsClassName: 'comment-range-glyph comment-diff-added' }; this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions); + + const hoverDecorationOptions: IModelDecorationOptions = { + description: CommentingRangeDecorator.description, + isWholeLine: true, + linesDecorationsClassName: `comment-range-glyph comment-diff-added line-hover` + }; + + this.hoverDecorationOptions = ModelDecorationOptions.createDynamic(hoverDecorationOptions); + } + + public updateHover(hoverLine?: number) { + if (this._editor && this._infos && (hoverLine !== this._lastHover)) { + this._doUpdate(this._editor, this._infos, hoverLine); + } + this._lastHover = hoverLine ?? -1; } public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { + this._editor = editor; + this._infos = commentInfos; + this._doUpdate(editor, commentInfos); + } + + private _doUpdate(editor: ICodeEditor, commentInfos: ICommentInfo[], hoverLine: number = -1) { let model = editor.getModel(); if (!model) { return; @@ -118,22 +161,47 @@ class CommentingRangeDecorator { let commentingRangeDecorations: CommentingRangeDecoration[] = []; for (const info of commentInfos) { info.commentingRanges.ranges.forEach(range => { - commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); + if ((range.startLineNumber <= hoverLine) && (range.endLineNumber >= hoverLine)) { + const beforeRange = new Range(range.startLineNumber, 1, hoverLine, 1); + const hoverRange = new Range(hoverLine, 1, hoverLine, 1); + const afterRange = new Range(hoverLine, 1, range.endLineNumber, 1); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, beforeRange, this.decorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, hoverRange, this.hoverDecorationOptions, info.commentingRanges, true)); + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, afterRange, this.decorationOptions, info.commentingRanges, true)); + } else { + commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label, range, this.decorationOptions, info.commentingRanges)); + } }); } - let oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id); - editor.deltaDecorations(oldDecorations, []); + this.decorationIds = editor.deltaDecorations(this.decorationIds, commentingRangeDecorations); + commentingRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + const rangesDifference = this.commentingRangeDecorations.length - commentingRangeDecorations.length; this.commentingRangeDecorations = commentingRangeDecorations; + if (rangesDifference) { + this._onDidChangeDecorationsCount.fire(this.commentingRangeDecorations.length); + } } public getMatchedCommentAction(line: number) { + // keys is ownerId + const foundHoverActions = new Map(); let result = []; for (const decoration of this.commentingRangeDecorations) { const range = decoration.getActiveRange(); if (range && range.startLineNumber <= line && line <= range.endLineNumber) { - result.push(decoration.getCommentAction()); + // We can have 3 commenting ranges that match from the same owner because of how + // the line hover decoration is done. We only want to use the action from 1 of them. + const action = decoration.getCommentAction(); + if (decoration.isHover) { + if (foundHoverActions.get(action.ownerId) === action.commentingRangesInfo) { + continue; + } else { + foundHoverActions.set(action.ownerId, action.commentingRangesInfo); + } + } + result.push(action); } } @@ -145,6 +213,11 @@ class CommentingRangeDecorator { } } +const ActiveCursorHasCommentingRange = new RawContextKey('activeCursorHasCommentingRange', false, { + description: nls.localize('hasCommentingRange', "Whether the position at the active cursor has a commenting range"), + type: 'boolean' +}); + export class CommentController implements IEditorContribution { private readonly globalToDispose = new DisposableStore(); private readonly localToDispose = new DisposableStore(); @@ -160,6 +233,8 @@ export class CommentController implements IEditorContribution { private _computeCommentingRangePromise!: CancelablePromise | null; private _computeCommentingRangeScheduler!: Delayer> | null; private _pendingCommentCache: { [key: string]: { [key: string]: string } }; + private _editorDisposables: IDisposable[] | undefined; + private _activeCursorHasCommentingRange: IContextKey; constructor( editor: ICodeEditor, @@ -167,12 +242,16 @@ export class CommentController implements IEditorContribution { @IInstantiationService private readonly instantiationService: IInstantiationService, @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IContextMenuService readonly contextMenuService: IContextMenuService, - @IQuickInputService private readonly quickInputService: IQuickInputService + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IViewsService private readonly viewsService: IViewsService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService readonly contextKeyService: IContextKeyService ) { this._commentInfos = []; this._commentWidgets = []; this._pendingCommentCache = {}; this._computePromise = null; + this._activeCursorHasCommentingRange = ActiveCursorHasCommentingRange.bindTo(contextKeyService); if (editor instanceof EmbeddedCodeEditorWidget) { return; @@ -181,6 +260,13 @@ export class CommentController implements IEditorContribution { this.editor = editor; this._commentingRangeDecorator = new CommentingRangeDecorator(); + this.globalToDispose.add(this._commentingRangeDecorator.onDidChangeDecorationsCount(count => { + if (count === 0) { + this.clearEditorListeners(); + } else if (!this._editorDisposables) { + this.registerEditorListeners(); + } + })); this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => { delete this._pendingCommentCache[ownerId]; @@ -201,12 +287,45 @@ export class CommentController implements IEditorContribution { this.beginCompute(); } + private registerEditorListeners() { + this._editorDisposables = []; + this._editorDisposables.push(this.editor.onMouseMove(e => this.onEditorMouseMove(e))); + this._editorDisposables.push(this.editor.onDidChangeCursorPosition(e => this.onEditorChangeCursorPosition(e.position))); + this._editorDisposables.push(this.editor.onDidFocusEditorWidget(() => this.onEditorChangeCursorPosition(this.editor.getPosition()))); + } + + private clearEditorListeners() { + this._editorDisposables?.forEach(disposable => disposable.dispose()); + this._editorDisposables = undefined; + } + + private onEditorMouseMove(e: IEditorMouseEvent): void { + this._commentingRangeDecorator.updateHover(e.target.position?.lineNumber); + } + + private onEditorChangeCursorPosition(e: Position | null) { + const decorations = e ? this.editor.getDecorationsInRange(Range.fromPositions(e, { column: -1, lineNumber: e.lineNumber })) : undefined; + let hasCommentingRange = false; + if (decorations) { + for (const decoration of decorations) { + if (decoration.options.description === CommentGlyphWidget.description) { + // We don't allow multiple comments on the same line. + hasCommentingRange = false; + break; + } else if (decoration.options.description === CommentingRangeDecorator.description) { + hasCommentingRange = true; + } + } + } + this._activeCursorHasCommentingRange.set(hasCommentingRange); + } + private beginCompute(): Promise { this._computePromise = createCancelablePromise(token => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; if (editorURI) { - return this.commentService.getComments(editorURI); + return this.commentService.getDocumentComments(editorURI); } return Promise.resolve([]); @@ -229,7 +348,7 @@ export class CommentController implements IEditorContribution { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; if (editorURI) { - return this.commentService.getComments(editorURI); + return this.commentService.getDocumentComments(editorURI); } return Promise.resolve([]); @@ -265,12 +384,21 @@ export class CommentController implements IEditorContribution { } public nextCommentThread(): void { + this._findNearestCommentThread(); + } + + private _findNearestCommentThread(reverse?: boolean): void { if (!this._commentWidgets.length || !this.editor.hasModel()) { return; } const after = this.editor.getSelection().getEndPosition(); const sortedWidgets = this._commentWidgets.sort((a, b) => { + if (reverse) { + const temp = a; + a = b; + b = temp; + } if (a.commentThread.range.startLineNumber < b.commentThread.range.startLineNumber) { return -1; } @@ -291,32 +419,42 @@ export class CommentController implements IEditorContribution { }); let idx = findFirstInSorted(sortedWidgets, widget => { - if (widget.commentThread.range.startLineNumber > after.lineNumber) { + let lineValueOne = reverse ? after.lineNumber : widget.commentThread.range.startLineNumber; + let lineValueTwo = reverse ? widget.commentThread.range.startLineNumber : after.lineNumber; + let columnValueOne = reverse ? after.column : widget.commentThread.range.startColumn; + let columnValueTwo = reverse ? widget.commentThread.range.startColumn : after.column; + if (lineValueOne > lineValueTwo) { return true; } - if (widget.commentThread.range.startLineNumber < after.lineNumber) { + if (lineValueOne < lineValueTwo) { return false; } - if (widget.commentThread.range.startColumn > after.column) { + if (columnValueOne > columnValueTwo) { return true; } return false; }); + let nextWidget: ReviewZoneWidget; if (idx === this._commentWidgets.length) { - this._commentWidgets[0].reveal(); - this.editor.setSelection(this._commentWidgets[0].commentThread.range); + nextWidget = this._commentWidgets[0]; } else { - sortedWidgets[idx].reveal(); - this.editor.setSelection(sortedWidgets[idx].commentThread.range); + nextWidget = sortedWidgets[idx]; } + this.editor.setSelection(nextWidget.commentThread.range); + nextWidget.reveal(undefined, true); + } + + public previousCommentThread(): void { + this._findNearestCommentThread(true); } public dispose(): void { this.globalToDispose.dispose(); this.localToDispose.dispose(); + this._editorDisposables?.forEach(disposable => disposable.dispose()); this._commentWidgets.forEach(widget => widget.dispose()); @@ -330,6 +468,10 @@ export class CommentController implements IEditorContribution { this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e))); this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e))); + if (this._editorDisposables) { + this.clearEditorListeners(); + this.registerEditorListeners(); + } this._computeCommentingRangeScheduler = new Delayer(200); this.localToDispose.add({ @@ -373,33 +515,42 @@ export class CommentController implements IEditorContribution { }); changed.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); - if (matchedZones.length) { - let matchedZone = matchedZones[0]; - matchedZone.update(thread); + if (thread.isDocumentCommentThread()) { + let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + let matchedZone = matchedZones[0]; + matchedZone.update(thread); + } } }); added.forEach(thread => { - let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); - if (matchedZones.length) { - return; + if (thread.isDocumentCommentThread()) { + let matchedZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.threadId === thread.threadId); + if (matchedZones.length) { + return; + } + + let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + + if (matchedNewCommentThreadZones.length) { + matchedNewCommentThreadZones[0].update(thread); + return; + } + + const pendingCommentText = this._pendingCommentCache[e.owner] && this._pendingCommentCache[e.owner][thread.threadId!]; + this.displayCommentThread(e.owner, thread, pendingCommentText); + this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); } - - let matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); - - if (matchedNewCommentThreadZones.length) { - matchedNewCommentThreadZones[0].update(thread); - return; - } - - const pendingCommentText = this._pendingCommentCache[e.owner] && this._pendingCommentCache[e.owner][thread.threadId!]; - this.displayCommentThread(e.owner, thread, pendingCommentText); - this._commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread); }); })); - this.beginCompute(); + this.beginCompute().then(() => { + if (this._commentWidgets.length + && (this.configurationService.getValue(COMMENTS_SECTION).openView === 'file')) { + this.viewsService.openView(COMMENTS_VIEW_ID); + } + }); } private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | null): void { @@ -630,16 +781,24 @@ export class CommentController implements IEditorContribution { this._commentWidgets = []; } + + public hasComments(): boolean { + return !!this._commentWidgets.length; + } } export class NextCommentThreadAction extends EditorAction { - constructor() { super({ id: 'editor.action.nextCommentThreadAction', label: nls.localize('nextCommentThreadAction', "Go to Next Comment Thread"), alias: 'Go to Next Comment Thread', precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyMod.Alt | KeyCode.F9, + weight: KeybindingWeight.EditorContrib + } }); } @@ -651,12 +810,37 @@ export class NextCommentThreadAction extends EditorAction { } } +export class PreviousCommentThreadAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.previousCommentThreadAction', + label: nls.localize('previousCommentThreadAction', "Go to Previous Comment Thread"), + alias: 'Go to Previous Comment Thread', + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.focus, + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F9, + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + let controller = CommentController.get(editor); + if (controller) { + controller.previousCommentThread(); + } + } +} + registerEditorContribution(ID, CommentController); registerEditorAction(NextCommentThreadAction); +registerEditorAction(PreviousCommentThreadAction); +const ADD_COMMENT_COMMAND = 'workbench.action.addComment'; CommandsRegistry.registerCommand({ - id: 'workbench.action.addComment', + id: ADD_COMMENT_COMMAND, handler: (accessor) => { const activeEditor = getActiveEditor(accessor); if (!activeEditor) { @@ -673,6 +857,15 @@ CommandsRegistry.registerCommand({ } }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ADD_COMMENT_COMMAND, + title: nls.localize('comments.addCommand', "Add Comment on Current Line"), + category: 'Comments' + }, + when: ActiveCursorHasCommentingRange +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'workbench.action.submitComment', weight: KeybindingWeight.EditorContrib, @@ -731,7 +924,7 @@ registerThemingParticipant((theme, collector) => { const monacoEditorBackground = theme.getColor(peekViewTitleBackground); if (monacoEditorBackground) { collector.addRule( - `.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` + + `.review-widget .body .comment-form .review-thread-reply-button {` + ` background-color: ${monacoEditorBackground}` + `}` ); @@ -740,10 +933,10 @@ registerThemingParticipant((theme, collector) => { const monacoEditorForeground = theme.getColor(editorForeground); if (monacoEditorForeground) { collector.addRule( - `.monaco-editor .review-widget .body .monaco-editor {` + + `.review-widget .body .monaco-editor {` + ` color: ${monacoEditorForeground}` + `}` + - `.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {` + + `.review-widget .body .comment-form .review-thread-reply-button {` + ` color: ${monacoEditorForeground};` + ` font-size: inherit` + `}` @@ -758,7 +951,7 @@ registerThemingParticipant((theme, collector) => { ` 0% { background: ${selectionBackground}; }` + ` 100% { background: transparent; }` + `}` + - `.monaco-editor .review-widget .body .review-comment.focus {` + + `.review-widget .body .review-comment.focus {` + ` animation: monaco-review-widget-focus 3s ease 0s;` + `}` ); @@ -784,11 +977,11 @@ registerThemingParticipant((theme, collector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { - collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground};}`); + collector.addRule(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.active:hover { background-color: ${statusBarItemHoverBackground};}`); } const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND); if (statusBarItemActiveBackground) { - collector.addRule(`.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`); + collector.addRule(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label:active { background-color: ${statusBarItemActiveBackground}; border: 1px solid transparent;}`); } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index de0206fb60c..ad852ea3b7e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { basename } from 'vs/base/common/resources'; -import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CommentNode, CommentsModel, ResourceWithCommentThreads, ICommentThreadChangedEvent } from 'vs/workbench/contrib/comments/common/commentModel'; @@ -29,6 +29,8 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { Codicon } from 'vs/base/common/codicons'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { TextModel } from 'vs/editor/common/model/textModel'; const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); @@ -211,18 +213,24 @@ export class CommentsPanel extends ViewPane { const range = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].range : element.range; - const activeEditor = this.editorService.activeEditor; - let currentActiveResource = activeEditor ? activeEditor.resource : undefined; - if (this.uriIdentityService.extUri.isEqual(element.resource, currentActiveResource)) { - const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; - const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; - const control = this.editorService.activeTextEditorControl; - if (threadToReveal && isCodeEditor(control)) { - const controller = CommentController.get(control); - controller?.revealCommentThread(threadToReveal, commentToReveal, false); - } + const activeEditor = this.editorService.activeTextEditorControl; + // If the active editor is a diff editor where one of the sides has the comment, + // then we try to reveal the comment in the diff editor. + let currentActiveResources: IEditor[] = isDiffEditor(activeEditor) ? [activeEditor.getOriginalEditor(), activeEditor.getModifiedEditor()] + : (activeEditor ? [activeEditor] : []); - return true; + for (const editor of currentActiveResources) { + const model = editor.getModel(); + if ((model instanceof TextModel) && this.uriIdentityService.extUri.isEqual(element.resource, model.uri)) { + const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; + const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; + if (threadToReveal && isCodeEditor(editor)) { + const controller = CommentController.get(editor); + controller?.revealCommentThread(threadToReveal, commentToReveal, false); + } + + return true; + } } const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index d093db1a8d6..791a2b70e7b 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -3,60 +3,60 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor .review-widget { +.review-widget { width: 100%; position: absolute; } -.monaco-editor .review-widget .hidden { +.review-widget .hidden { display: none !important; } -.monaco-editor .review-widget .body { +.review-widget .body { overflow: hidden; } -.monaco-editor .review-widget .body .review-comment { +.review-widget .body .review-comment { padding: 8px 16px 8px 20px; display: flex; } -.monaco-editor .review-widget .body .review-comment .comment-actions { +.review-widget .body .review-comment .comment-actions { margin-left: auto; } -.monaco-editor .review-widget .body .review-comment .comment-actions .monaco-toolbar { +.review-widget .body .review-comment .comment-actions .monaco-toolbar { height: 21px; } -.monaco-editor .review-widget .body .review-comment .comment-title { +.review-widget .body .review-comment .comment-title { display: flex; width: 100%; } -.monaco-editor .review-widget .body .review-comment .comment-title .action-label.codicon { +.review-widget .body .review-comment .comment-title .action-label.codicon { line-height: 18px; } -.monaco-editor .review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more { +.review-widget .body .review-comment .comment-title .monaco-dropdown .toolbar-toggle-more { width: 16px; height: 18px; line-height: 18px; vertical-align: middle; } -.monaco-editor .review-widget .body .comment-body blockquote { +.review-widget .body .comment-body blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; border-left-style: solid; } -.monaco-editor .review-widget .body .review-comment .avatar-container { +.review-widget .body .review-comment .avatar-container { margin-top: 4px !important; } -.monaco-editor .review-widget .body .review-comment .avatar-container img.avatar { +.review-widget .body .review-comment .avatar-container img.avatar { height: 28px; width: 28px; display: inline-block; @@ -67,7 +67,7 @@ border-style: none; } -.monaco-editor .review-widget .body .comment-reactions .monaco-text-button { +.review-widget .body .comment-reactions .monaco-text-button { margin: 0 7px 0 0; width: 30px; background-color: transparent; @@ -75,7 +75,7 @@ border-radius: 3px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents { +.review-widget .body .review-comment .review-comment-contents { padding-left: 20px; user-select: text; -webkit-user-select: text; @@ -83,41 +83,41 @@ overflow: hidden; } -.monaco-editor .review-widget .body pre { +.review-widget .body pre { overflow: auto; word-wrap: normal; white-space: pre; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .author { +.review-widget .body .review-comment .review-comment-contents .author { line-height: 22px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .isPending { +.review-widget .body .review-comment .review-comment-contents .isPending { line-height: 22px; margin: 0 5px 0 5px; padding: 0 2px 0 2px; font-style: italic; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .timestamp { +.review-widget .body .review-comment .review-comment-contents .timestamp { line-height: 22px; margin: 0 5px 0 5px; padding: 0 2px 0 2px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-body { +.review-widget .body .review-comment .review-comment-contents .comment-body { padding-top: 4px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions { +.review-widget .body .review-comment .review-comment-contents .comment-reactions { margin-top: 8px; min-height: 25px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label { padding: 1px 4px; white-space: pre; text-align: center; @@ -125,7 +125,7 @@ display: flex; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-icon { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-icon { background-size: 14px; background-position: left center; background-repeat: no-repeat; @@ -136,12 +136,12 @@ margin-right: 4px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-label { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item .action-label .reaction-label { line-height: 20px; margin-right: 4px; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.toolbar-toggle-pickReactions { display: none; background-size: 16px; width: 26px; @@ -152,12 +152,12 @@ border: none; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions:hover .action-item a.action-label.toolbar-toggle-pickReactions { +.review-widget .body .review-comment .review-comment-contents .comment-reactions:hover .action-item a.action-label.toolbar-toggle-pickReactions { display: inline-block; background-size: 16px; } -.monaco-editor .review-widget .body .review-comment .comment-title .action-label { +.review-widget .body .review-comment .comment-title .action-label { display: block; height: 16px; line-height: 16px; @@ -166,63 +166,63 @@ background-repeat: no-repeat; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border: 1px solid; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled { +.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled { opacity: 0.6; } -.monaco-editor .review-widget .body .review-comment .review-comment-contents a { +.review-widget .body .review-comment .review-comment-contents a { cursor: pointer; } -.monaco-editor .review-widget .body .comment-body p, -.monaco-editor .review-widget .body .comment-body ul { +.review-widget .body .comment-body p, +.review-widget .body .comment-body ul { margin: 8px 0; } -.monaco-editor .review-widget .body .comment-body p:first-child, -.monaco-editor .review-widget .body .comment-body ul:first-child { +.review-widget .body .comment-body p:first-child, +.review-widget .body .comment-body ul:first-child { margin-top: 0; } -.monaco-editor .review-widget .body .comment-body p:last-child, -.monaco-editor .review-widget .body.comment-body ul:last-child { +.review-widget .body .comment-body p:last-child, +.review-widget .body.comment-body ul:last-child { margin-bottom: 0; } -.monaco-editor .review-widget .body .comment-body ul { +.review-widget .body .comment-body ul { padding-left: 20px; } -.monaco-editor .review-widget .body .comment-body li > p { +.review-widget .body .comment-body li > p { margin-bottom: 0; } -.monaco-editor .review-widget .body .comment-body li > ul { +.review-widget .body .comment-body li > ul { margin-top: 0; } -.monaco-editor .review-widget .body .comment-body code { +.review-widget .body .comment-body code { border-radius: 3px; padding: 0 0.4em; } -.monaco-editor .review-widget .body .comment-body span { +.review-widget .body .comment-body span { white-space: pre; } -.monaco-editor .review-widget .body .comment-body img { +.review-widget .body .comment-body img { max-width: 100%; } -.monaco-editor .review-widget .body .comment-form { +.review-widget .body .comment-form { margin: 8px 20px; } -.monaco-editor .review-widget .validation-error { +.review-widget .validation-error { display: inline-block; overflow: hidden; text-align: left; @@ -237,17 +237,17 @@ word-wrap: break-word; } -.monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button { +.review-widget .body .comment-form.expand .review-thread-reply-button { display: none; } -.monaco-editor .review-widget .body .comment-form.expand .monaco-editor, -.monaco-editor .review-widget .body .comment-form.expand .form-actions { +.review-widget .body .comment-form.expand .monaco-editor, +.review-widget .body .comment-form.expand .form-actions { display: block; box-sizing: content-box; } -.monaco-editor .review-widget .body .comment-form .review-thread-reply-button { +.review-widget .body .comment-form .review-thread-reply-button { text-align: left; display: block; width: 100%; @@ -262,13 +262,13 @@ outline: 1px solid transparent; } -.monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus { +.review-widget .body .comment-form .review-thread-reply-button:focus { outline-style: solid; outline-width: 1px; } -.monaco-editor .review-widget .body .comment-form .monaco-editor, -.monaco-editor .review-widget .body .edit-container .monaco-editor { +.review-widget .body .comment-form .monaco-editor, +.review-widget .body .edit-container .monaco-editor { width: 100%; min-height: 90px; max-height: 500px; @@ -278,79 +278,79 @@ padding: 6px 0 6px 12px; } -.monaco-editor .review-widget .body .comment-form .monaco-editor, -.monaco-editor .review-widget .body .comment-form .form-actions { +.review-widget .body .comment-form .monaco-editor, +.review-widget .body .comment-form .form-actions { display: none; } -.monaco-editor .review-widget .body .comment-form .form-actions, -.monaco-editor .review-widget .body .edit-container .form-actions { +.review-widget .body .comment-form .form-actions, +.review-widget .body .edit-container .form-actions { overflow: auto; padding: 10px 0; } -.monaco-editor .review-widget .body .edit-container .form-actions { +.review-widget .body .edit-container .form-actions { display: flex; justify-content: flex-end; } -.monaco-editor .review-widget .body .edit-textarea { +.review-widget .body .edit-textarea { height: 90px; margin: 5px 0 10px 0; } -.monaco-editor .review-widget .body .comment-form .monaco-text-button, -.monaco-editor .review-widget .body .edit-container .monaco-text-button { +.review-widget .body .comment-form .monaco-text-button, +.review-widget .body .edit-container .monaco-text-button { width: auto; padding: 4px 10px; margin-left: 5px; margin-bottom: 5px; } -.monaco-editor .review-widget .body .comment-form .monaco-text-button { +.review-widget .body .comment-form .monaco-text-button { float: right; } -.monaco-editor .review-widget .head { +.review-widget .head { box-sizing: border-box; display: flex; height: 100%; } -.monaco-editor .review-widget .head .review-title { +.review-widget .head .review-title { display: inline-block; font-size: 13px; margin-left: 20px; cursor: default; } -.monaco-editor .review-widget .head .review-title .dirname:not(:empty) { +.review-widget .head .review-title .dirname:not(:empty) { font-size: 0.9em; margin-left: 0.5em; } -.monaco-editor .review-widget .head .review-actions { +.review-widget .head .review-actions { flex: 1; text-align: right; padding-right: 2px; } -.monaco-editor .review-widget .head .review-actions > .monaco-action-bar { +.review-widget .head .review-actions > .monaco-action-bar { display: inline-block; } -.monaco-editor .review-widget .head .review-actions > .monaco-action-bar, -.monaco-editor .review-widget .head .review-actions > .monaco-action-bar > .actions-container { +.review-widget .head .review-actions > .monaco-action-bar, +.review-widget .head .review-actions > .monaco-action-bar > .actions-container { height: 100%; } -.monaco-editor .review-widget .action-item { +.review-widget .action-item { min-width: 18px; min-height: 20px; margin-left: 4px; } -.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-label { +.review-widget .head .review-actions > .monaco-action-bar .action-label { width: 16px; margin: 0; line-height: inherit; @@ -358,11 +358,11 @@ background-position: center center; } -.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-label.codicon { +.review-widget .head .review-actions > .monaco-action-bar .action-label.codicon { margin: 0; } -.monaco-editor .review-widget > .body { +.review-widget > .body { border-top: 1px solid; position: relative; } @@ -388,6 +388,7 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.line-hover:before, .monaco-editor .comment-range-glyph.comment-thread:before { position: absolute; height: 100%; @@ -402,7 +403,8 @@ div.preview.inline .monaco-editor .comment-range-glyph { justify-content: center; } -.monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before { +.monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-diff-added.line-hover:before { content: "+"; } diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index 98ea69aa939..4a69955fd4a 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -20,7 +20,6 @@ import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/t import { IThemeService } from 'vs/platform/theme/common/themeService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; @@ -30,7 +29,6 @@ export const ctxCommentEditorFocused = new RawContextKey('commentEditor export class SimpleCommentEditor extends CodeEditorWidget { - private _parentEditor: ICodeEditor; private _parentThread: ICommentThreadWidget; private _commentEditorFocused: IContextKey; private _commentEditorEmpty: IContextKey; @@ -38,7 +36,6 @@ export class SimpleCommentEditor extends CodeEditorWidget { constructor( domElement: HTMLElement, options: IEditorOptions, - parentEditor: ICodeEditor, parentThread: ICommentThreadWidget, @IInstantiationService instantiationService: IInstantiationService, @ICodeEditorService codeEditorService: ICodeEditorService, @@ -66,7 +63,6 @@ export class SimpleCommentEditor extends CodeEditorWidget { this._commentEditorFocused = ctxCommentEditorFocused.bindTo(contextKeyService); this._commentEditorEmpty = CommentContextKeys.commentIsEmpty.bindTo(contextKeyService); this._commentEditorEmpty.set(!this.getValue()); - this._parentEditor = parentEditor; this._parentThread = parentThread; this._register(this.onDidFocusEditorWidget(_ => this._commentEditorFocused.set(true))); @@ -75,10 +71,6 @@ export class SimpleCommentEditor extends CodeEditorWidget { this._register(this.onDidBlurEditorWidget(_ => this._commentEditorFocused.reset())); } - getParentEditor(): ICodeEditor { - return this._parentEditor; - } - getParentThread(): ICommentThreadWidget { return this._parentThread; } diff --git a/src/vs/workbench/contrib/comments/browser/timestamp.ts b/src/vs/workbench/contrib/comments/browser/timestamp.ts index ad7ae7fbd38..cf68da654d6 100644 --- a/src/vs/workbench/contrib/comments/browser/timestamp.ts +++ b/src/vs/workbench/contrib/comments/browser/timestamp.ts @@ -7,8 +7,7 @@ import * as dom from 'vs/base/browser/dom'; import { fromNow } from 'vs/base/common/date'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - -const USE_RELATIVE_TIME_CONFIGURATION = 'comments.useRelativeTime'; +import { COMMENTS_SECTION, ICommentsConfiguration } from 'vs/workbench/contrib/comments/common/commentsConfiguration'; export class TimestampWidget extends Disposable { private _date: HTMLElement; @@ -24,7 +23,7 @@ export class TimestampWidget extends Disposable { } private get useRelativeTimeSetting(): boolean { - return this.configurationService.getValue(USE_RELATIVE_TIME_CONFIGURATION); + return this.configurationService.getValue(COMMENTS_SECTION).useRelativeTime; } public async setTimestamp(timestamp: Date | undefined) { diff --git a/src/vs/workbench/contrib/comments/common/commentModel.ts b/src/vs/workbench/contrib/comments/common/commentModel.ts index a9c9fbf27ec..b414fd9c8a4 100644 --- a/src/vs/workbench/contrib/comments/common/commentModel.ts +++ b/src/vs/workbench/contrib/comments/common/commentModel.ts @@ -6,10 +6,11 @@ import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Comment, CommentThread, CommentThreadChangedEvent } from 'vs/editor/common/languages'; -import { groupBy, flatten } from 'vs/base/common/arrays'; +import { groupBy } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { +export interface ICommentThreadChangedEvent extends CommentThreadChangedEvent { owner: string; } @@ -71,9 +72,16 @@ export class CommentsModel { this.commentThreadsMap = new Map(); } + private updateResourceCommentThreads() { + this.resourceCommentThreads = [...this.commentThreadsMap.values()].flat(); + this.resourceCommentThreads.sort((a, b) => { + return a.resource.toString() > b.resource.toString() ? 1 : -1; + }); + } + public setCommentThreads(owner: string, commentThreads: CommentThread[]): void { this.commentThreadsMap.set(owner, this.groupByResource(owner, commentThreads)); - this.resourceCommentThreads = flatten([...this.commentThreadsMap.values()]); + this.updateResourceCommentThreads(); } public updateCommentThreads(event: ICommentThreadChangedEvent): boolean { @@ -97,33 +105,37 @@ export class CommentsModel { }); changed.forEach(thread => { - // Find resource that has the comment thread - const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource); - const matchingResourceData = threadsForOwner[matchingResourceIndex]; + if (thread.isDocumentCommentThread()) { + // Find resource that has the comment thread + const matchingResourceIndex = threadsForOwner.findIndex((resourceData) => resourceData.id === thread.resource); + const matchingResourceData = threadsForOwner[matchingResourceIndex]; - // Find comment node on resource that is that thread and replace it - const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId); - if (index >= 0) { - matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread); - } else if (thread.comments && thread.comments.length) { - matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread)); + // Find comment node on resource that is that thread and replace it + const index = matchingResourceData.commentThreads.findIndex((commentThread) => commentThread.threadId === thread.threadId); + if (index >= 0) { + matchingResourceData.commentThreads[index] = ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread); + } else if (thread.comments && thread.comments.length) { + matchingResourceData.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, URI.parse(matchingResourceData.id), thread)); + } } }); added.forEach(thread => { - const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource); - if (existingResource.length) { - const resource = existingResource[0]; - if (thread.comments && thread.comments.length) { - resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread)); + if (thread.isDocumentCommentThread()) { + const existingResource = threadsForOwner.filter(resourceWithThreads => resourceWithThreads.resource.toString() === thread.resource); + if (existingResource.length) { + const resource = existingResource[0]; + if (thread.comments && thread.comments.length) { + resource.commentThreads.push(ResourceWithCommentThreads.createCommentNode(owner, resource.resource, thread)); + } + } else { + threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread])); } - } else { - threadsForOwner.push(new ResourceWithCommentThreads(owner, URI.parse(thread.resource!), [thread])); } }); this.commentThreadsMap.set(owner, threadsForOwner); - this.resourceCommentThreads = flatten([...this.commentThreadsMap.values()]); + this.updateResourceCommentThreads(); return removed.length > 0 || changed.length > 0 || added.length > 0; } diff --git a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts new file mode 100644 index 00000000000..1ef2ba67cb7 --- /dev/null +++ b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface ICommentsConfiguration { + openView: 'never' | 'file'; + useRelativeTime: boolean; +} + +export const COMMENTS_SECTION = 'comments'; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index f6fe05027fa..119a41914f1 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -17,12 +17,11 @@ import { FileSystemProviderCapabilities, IFileService } from 'vs/platform/files/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, GroupIdentifier, IRevertOptions, ISaveOptions, isEditorInputWithOptionsAndGroup, IUntypedEditorInput, Verbosity } from 'vs/workbench/common/editor'; +import { EditorInputCapabilities, GroupIdentifier, IMoveResult, IRevertOptions, ISaveOptions, IUntypedEditorInput, Verbosity } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IWebviewService, IOverlayWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewWorkbenchService, LazilyResolvedWebviewEditorInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; -import { IEditorResolverService } from 'vs/workbench/services/editor/common/editorResolverService'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @@ -73,7 +72,6 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @ILabelService private readonly labelService: ILabelService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IEditorResolverService private readonly editorResolverService: IEditorResolverService, @IUndoRedoService private readonly undoRedoService: IUndoRedoService, @IFileService private readonly fileService: IFileService ) { @@ -249,7 +247,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return this._modelRef.object.isDirty(); } - public override async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + public override async save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { if (!this._modelRef) { return undefined; } @@ -259,14 +257,15 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return undefined; // save cancelled } + // Different URIs == untyped input returned to allow resolver to possibly resolve to a different editor type if (!isEqual(target, this.resource)) { - return CustomEditorInput.create(this.instantiationService, target, this.viewType, groupId); + return { resource: target }; } return this; } - public override async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + public override async saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { if (!this._modelRef) { return undefined; } @@ -319,32 +318,9 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return null; } - public override async rename(group: GroupIdentifier, newResource: URI): Promise<{ editor: EditorInput } | undefined> { - // See if we can keep using the same custom editor provider - const editorInfo = this.customEditorService.getCustomEditor(this.viewType); - if (editorInfo?.matches(newResource)) { - return { editor: this.doMove(group, newResource) }; - } - - const resolvedEditor = await this.editorResolverService.resolveEditor({ resource: newResource, options: { override: DEFAULT_EDITOR_ASSOCIATION.id } }, undefined); - return isEditorInputWithOptionsAndGroup(resolvedEditor) ? { editor: resolvedEditor.editor } : undefined; - } - - private doMove(group: GroupIdentifier, newResource: URI): EditorInput { - if (!this._moveHandler) { - return CustomEditorInput.create(this.instantiationService, newResource, this.viewType, group, { oldResource: this.resource }); - } - - this._moveHandler(newResource); - const newEditor = this.instantiationService.createInstance(CustomEditorInput, - newResource, - this.viewType, - this.id, - undefined!, // this webview is replaced in the transfer call - { startsDirty: this._defaultDirtyState, backupId: this._backupId }); - this.transfer(newEditor); - newEditor.updateGroup(group); - return newEditor; + public override async rename(group: GroupIdentifier, newResource: URI): Promise { + // We return an untyped editor input which can then be resolved in the editor service + return { editor: { resource: newResource } }; } public undo(): void | Promise { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 9861c11998f..db0ce90b3b1 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -148,7 +148,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ): DiffEditorInput { const modifiedOverride = CustomEditorInput.create(this.instantiationService, assertIsDefined(editor.modified.resource), editorID, group.id, { customClasses: 'modified' }); const originalOverride = CustomEditorInput.create(this.instantiationService, assertIsDefined(editor.original.resource), editorID, group.id, { customClasses: 'original' }); - return this.instantiationService.createInstance(DiffEditorInput, undefined, undefined, originalOverride, modifiedOverride, true); + return this.instantiationService.createInstance(DiffEditorInput, editor.label, editor.description, originalOverride, modifiedOverride, true); } public get models() { return this._models; } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 807a53b14b2..762b74c8201 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -741,8 +741,8 @@ registerThemingParticipant((theme, collector) => { } }); -const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hc: '#E51400' }, nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); -const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); -const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hc: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); -const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hc: '#FFCC00' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); -const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hc: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); +const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', { dark: '#E51400', light: '#E51400', hcDark: '#E51400', hcLight: '#E51400' }, nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.')); +const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.')); +const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', { dark: '#848484', light: '#848484', hcDark: '#848484', hcLight: '#848484' }, nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.')); +const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.')); +const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', { dark: '#89D185', light: '#89D185', hcDark: '#89D185', hcLight: '#89D185' }, nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.')); diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 045126154c7..b858ddb0659 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -19,8 +19,8 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ILogService } from 'vs/platform/log/common/log'; -export const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hc: '#ffff0033' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); -export const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hc: '#7abd7a4d' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.')); +export const topStackFrameColor = registerColor('editor.stackFrameHighlightBackground', { dark: '#ffff0033', light: '#ffff6673', hcDark: '#ffff0033', hcLight: '#ffff6673' }, localize('topStackFrameLineHighlight', 'Background color for the highlight of line at the top stack frame position.')); +export const focusedStackFrameColor = registerColor('editor.focusedStackFrameHighlightBackground', { dark: '#7abd7a4d', light: '#cee7ce73', hcDark: '#7abd7a4d', hcLight: '#cee7ce73' }, localize('focusedStackFrameLineHighlight', 'Background color for the highlight of line at focused stack frame position.')); const stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; // we need a separate decoration for glyph margin, since we do not want it on each line of a multi line statement. diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 60129cb91a3..687732063d7 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -587,11 +587,11 @@ class SessionsRenderer implements ICompressibleTreeRenderer, _index: number, data: IThreadTemplateData): void { const thread = element.element; - data.thread.title = localize('thread', "Thread"); + data.thread.title = thread.name; data.label.set(thread.name, createMatches(element.filterData)); data.stateLabel.textContent = thread.stateLabel; data.stateLabel.classList.toggle('exception', thread.stoppedDetails?.reason === 'exception'); diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index 097966810dd..affb86d4e98 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -8,98 +8,111 @@ import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/ import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; export const debugToolBarBackground = registerColor('debugToolBar.background', { dark: '#333333', light: '#F3F3F3', - hc: '#000000' + hcDark: '#000000', + hcLight: '#FFFFFF' }, localize('debugToolBarBackground', "Debug toolbar background color.")); export const debugToolBarBorder = registerColor('debugToolBar.border', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, localize('debugToolBarBorder', "Debug toolbar border color.")); export const debugIconStartForeground = registerColor('debugIcon.startForeground', { dark: '#89D185', light: '#388A34', - hc: '#89D185' + hcDark: '#89D185', + hcLight: '#388A34' }, localize('debugIcon.startForeground', "Debug toolbar icon for start debugging.")); export function registerColors() { - const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hc: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Variables or Watch view).'); - const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hc: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Variables or Watch view).'); - const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hc: '#f48771' }, 'Foreground color for strings in the debug views (ie. the Variables or Watch view).'); - const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hc: '#75bdfe' }, 'Foreground color for booleans in the debug views (ie. the Variables or Watch view).'); - const debugTokenExpressionNumber = registerColor('debugTokenExpression.number', { dark: '#b5cea8', light: '#098658', hc: '#89d185' }, 'Foreground color for numbers in the debug views (ie. the Variables or Watch view).'); - const debugTokenExpressionError = registerColor('debugTokenExpression.error', { dark: '#f48771', light: '#e51400', hc: '#f48771' }, 'Foreground color for expression errors in the debug views (ie. the Variables or Watch view) and for error logs shown in the debug console.'); + const debugTokenExpressionName = registerColor('debugTokenExpression.name', { dark: '#c586c0', light: '#9b46b0', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token names shown in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionValue = registerColor('debugTokenExpression.value', { dark: '#cccccc99', light: '#6c6c6ccc', hcDark: foreground, hcLight: foreground }, 'Foreground color for the token values shown in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionString = registerColor('debugTokenExpression.string', { dark: '#ce9178', light: '#a31515', hcDark: '#f48771', hcLight: '#a31515' }, 'Foreground color for strings in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean', { dark: '#4e94ce', light: '#0000ff', hcDark: '#75bdfe', hcLight: '#0000ff' }, 'Foreground color for booleans in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionNumber = registerColor('debugTokenExpression.number', { dark: '#b5cea8', light: '#098658', hcDark: '#89d185', hcLight: '#098658' }, 'Foreground color for numbers in the debug views (ie. the Variables or Watch view).'); + const debugTokenExpressionError = registerColor('debugTokenExpression.error', { dark: '#f48771', light: '#e51400', hcDark: '#f48771', hcLight: '#e51400' }, 'Foreground color for expression errors in the debug views (ie. the Variables or Watch view) and for error logs shown in the debug console.'); - const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hc: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); - const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hc: '#6C2022' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); - const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); - const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hc: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); - const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hc: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); + const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hcDark: foreground, hcLight: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); + const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hcDark: '#6C2022', hcLight: '#A31515' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); + const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hcDark: '#88888844', hcLight: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); + const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hcDark: '#569CD6', hcLight: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); - const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: foreground }, 'Foreground color for info messages in debug REPL console.'); - const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: '#008000' }, 'Foreground color for warning messages in debug REPL console.'); - const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hc: errorForeground }, 'Foreground color for error messages in debug REPL console.'); - const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); - const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hcDark: foreground, hcLight: foreground }, 'Foreground color for info messages in debug REPL console.'); + const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hcDark: '#008000', hcLight: editorWarningForeground }, 'Foreground color for warning messages in debug REPL console.'); + const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hcDark: errorForeground, hcLight: errorForeground }, 'Foreground color for error messages in debug REPL console.'); + const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for source filenames in debug REPL console.'); + const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground }, 'Foreground color for debug console input marker icon.'); const debugIconPauseForeground = registerColor('debugIcon.pauseForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.pauseForeground', "Debug toolbar icon for pause.")); const debugIconStopForeground = registerColor('debugIcon.stopForeground', { dark: '#F48771', light: '#A1260D', - hc: '#F48771' + hcDark: '#F48771', + hcLight: '#A1260D' }, localize('debugIcon.stopForeground', "Debug toolbar icon for stop.")); const debugIconDisconnectForeground = registerColor('debugIcon.disconnectForeground', { dark: '#F48771', light: '#A1260D', - hc: '#F48771' + hcDark: '#F48771', + hcLight: '#A1260D' }, localize('debugIcon.disconnectForeground', "Debug toolbar icon for disconnect.")); const debugIconRestartForeground = registerColor('debugIcon.restartForeground', { dark: '#89D185', light: '#388A34', - hc: '#89D185' + hcDark: '#89D185', + hcLight: '#388A34' }, localize('debugIcon.restartForeground', "Debug toolbar icon for restart.")); const debugIconStepOverForeground = registerColor('debugIcon.stepOverForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.stepOverForeground', "Debug toolbar icon for step over.")); const debugIconStepIntoForeground = registerColor('debugIcon.stepIntoForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.stepIntoForeground', "Debug toolbar icon for step into.")); const debugIconStepOutForeground = registerColor('debugIcon.stepOutForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.stepOutForeground', "Debug toolbar icon for step over.")); const debugIconContinueForeground = registerColor('debugIcon.continueForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.continueForeground', "Debug toolbar icon for continue.")); const debugIconStepBackForeground = registerColor('debugIcon.stepBackForeground', { dark: '#75BEFF', light: '#007ACC', - hc: '#75BEFF' + hcDark: '#75BEFF', + hcLight: '#007ACC' }, localize('debugIcon.stepBackForeground', "Debug toolbar icon for step back.")); registerThemingParticipant((theme, collector) => { @@ -181,6 +194,15 @@ export function registerColors() { `); } + // Use fully-opaque colors for line-number badges + if (isHighContrast(theme.type)) { + collector.addRule(` + .debug-pane .line-number { + background-color: ${badgeBackgroundColor}; + color: ${badgeForegroundColor}; + }`); + } + const tokenNameColor = theme.getColor(debugTokenExpressionName)!; const tokenValueColor = theme.getColor(debugTokenExpressionValue)!; const tokenStringColor = theme.getColor(debugTokenExpressionString)!; @@ -263,7 +285,8 @@ export function registerColors() { opacity: 0.4; } - .monaco-workbench.hc-black .repl .repl-tree .monaco-tl-contents .arrow { + .monaco-workbench.hc-black .repl .repl-tree .monaco-tl-contents .arrow, + .monaco-workbench.hc-light .repl .repl-tree .monaco-tl-contents .arrow { opacity: 1; } `); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 42d88028864..d8cfe6fb6cc 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -57,13 +57,15 @@ const DEAFULT_INLINE_DEBOUNCE_DELAY = 200; export const debugInlineForeground = registerColor('editor.inlineValuesForeground', { dark: '#ffffff80', light: '#00000080', - hc: '#ffffff80' + hcDark: '#ffffff80', + hcLight: '#00000080' }, nls.localize('editor.inlineValuesForeground', "Color for the debug inline value text.")); export const debugInlineBackground = registerColor('editor.inlineValuesBackground', { dark: '#ffc80033', light: '#ffc80033', - hc: '#ffc80033' + hcDark: '#ffc80033', + hcLight: '#ffc80033' }, nls.localize('editor.inlineValuesBackground', "Color for the debug inline value background.")); class InlineSegment { @@ -506,7 +508,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { closeExceptionWidget(): void { if (this.exceptionWidget) { - const shouldFocusEditor = this.exceptionWidget.hasfocus(); + const shouldFocusEditor = this.exceptionWidget.hasFocus(); this.exceptionWidget.dispose(); this.exceptionWidget = undefined; this.exceptionWidgetVisible.set(false); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index cc3a9e82af9..39b73ddbc29 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -619,7 +619,7 @@ export class DebugService implements IDebugService { const launchJsonExists = !!session.root && !!this.configurationService.getValue('launch', { resource: session.root.uri }); await this.telemetry.logDebugSessionStart(dbgr!, launchJsonExists); - if (forceFocus || !this.viewModel.focusedSession) { + if (forceFocus || !this.viewModel.focusedSession || (session.parentSession === this.viewModel.focusedSession && session.compact)) { await this.focusStackFrame(undefined, undefined, session); } } catch (err) { diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index 7cf0d618cd4..552a4f070c4 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -44,6 +44,7 @@ import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; interface IDisassembledInstructionEntry { allowBreakpoint: boolean; isBreakpointSet: boolean; + isBreakpointEnabled: boolean; instruction: DebugProtocol.DisassembledInstruction; instructionAddress?: bigint; } @@ -52,6 +53,7 @@ interface IDisassembledInstructionEntry { const disassemblyNotAvailable: IDisassembledInstructionEntry = { allowBreakpoint: false, isBreakpointSet: false, + isBreakpointEnabled: false, instruction: { address: '-1', instruction: localize('instructionNotAvailable', "Disassembly not available.") @@ -232,6 +234,7 @@ export class DisassemblyView extends EditorPane { const index = this.getIndexFromAddress(bp.instructionReference); if (index >= 0) { this._disassembledInstructions!.row(index).isBreakpointSet = true; + this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; changed = true; } } @@ -247,6 +250,18 @@ export class DisassemblyView extends EditorPane { } }); + bpEvent.changed?.forEach((bp) => { + if (bp instanceof InstructionBreakpoint) { + const index = this.getIndexFromAddress(bp.instructionReference); + if (index >= 0) { + if (this._disassembledInstructions!.row(index).isBreakpointEnabled !== bp.enabled) { + this._disassembledInstructions!.row(index).isBreakpointEnabled = bp.enabled; + changed = true; + } + } + } + }); + // get an updated list so that items beyond the current range would render when reached. this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints(); @@ -363,7 +378,7 @@ export class DisassemblyView extends EditorPane { } } - newEntries.push({ allowBreakpoint: true, isBreakpointSet: found !== undefined, instruction: instruction }); + newEntries.push({ allowBreakpoint: true, isBreakpointSet: found !== undefined, isBreakpointEnabled: !!found?.enabled, instruction: instruction }); } const specialEntriesToRemove = this._disassembledInstructions.length === 1 ? 1 : 0; @@ -467,6 +482,7 @@ class BreakpointRenderer implements ITableRenderer !!(sf && sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize')); + // Allow stack frame without source and with instructionReferencePointer as top stack frame when using disassembly view. + const firstAvailableStackFrame = callStack.find(sf => !!(sf && + ((this.stoppedDetails?.reason === 'instruction breakpoint' || (this.stoppedDetails?.reason === 'step' && this.lastSteppingGranularity === 'instruction')) && sf.instructionPointerReference) || + (sf.source && sf.source.available && sf.source.presentationHint !== 'deemphasize'))); return firstAvailableStackFrame || (callStack.length > 0 ? callStack[0] : undefined); } diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 540c79f99ea..242b3fd980c 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -3,14 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - /** Declaration module describing the VS Code debug protocol. Auto-generated from json schema. Do not edit manually. */ declare module DebugProtocol { /** Base class of requests, responses, and events. */ - export interface ProtocolMessage { + interface ProtocolMessage { /** Sequence number (also known as message ID). For protocol messages of type 'request' this ID can be used to cancel the request. */ seq: number; /** Message type. @@ -20,7 +19,7 @@ declare module DebugProtocol { } /** A client or debug adapter initiated request. */ - export interface Request extends ProtocolMessage { + interface Request extends ProtocolMessage { // type: 'request'; /** The command to execute. */ command: string; @@ -29,7 +28,7 @@ declare module DebugProtocol { } /** A debug adapter initiated event. */ - export interface Event extends ProtocolMessage { + interface Event extends ProtocolMessage { // type: 'event'; /** Type of event. */ event: string; @@ -38,7 +37,7 @@ declare module DebugProtocol { } /** Response for a request. */ - export interface Response extends ProtocolMessage { + interface Response extends ProtocolMessage { // type: 'response'; /** Sequence number of the corresponding request. */ request_seq: number; @@ -62,7 +61,7 @@ declare module DebugProtocol { } /** On error (whenever 'success' is false), the body can provide more details. */ - export interface ErrorResponse extends Response { + interface ErrorResponse extends Response { body: { /** An optional, structured error message. */ error?: Message; @@ -82,13 +81,13 @@ declare module DebugProtocol { The progress that got cancelled still needs to send a 'progressEnd' event back. A client should not assume that progress just got cancelled after sending the 'cancel' request. */ - export interface CancelRequest extends Request { + interface CancelRequest extends Request { // command: 'cancel'; arguments?: CancelArguments; } /** Arguments for 'cancel' request. */ - export interface CancelArguments { + interface CancelArguments { /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ @@ -100,7 +99,7 @@ declare module DebugProtocol { } /** Response to 'cancel' request. This is just an acknowledgement, so no body field is required. */ - export interface CancelResponse extends Response { + interface CancelResponse extends Response { } /** Event message for 'initialized' event type. @@ -114,7 +113,7 @@ declare module DebugProtocol { - frontend sends other future configuration requests - frontend sends one 'configurationDone' request to indicate the end of the configuration. */ - export interface InitializedEvent extends Event { + interface InitializedEvent extends Event { // event: 'initialized'; } @@ -122,7 +121,7 @@ declare module DebugProtocol { The event indicates that the execution of the debuggee has stopped due to some condition. This can be caused by a break point previously set, a stepping request has completed, by executing a debugger statement etc. */ - export interface StoppedEvent extends Event { + interface StoppedEvent extends Event { // event: 'stopped'; body: { /** The reason for the event. @@ -157,7 +156,7 @@ declare module DebugProtocol { Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. 'launch' or 'continue'. It is only necessary to send a 'continued' event if there was no previous request that implied this. */ - export interface ContinuedEvent extends Event { + interface ContinuedEvent extends Event { // event: 'continued'; body: { /** The thread which was continued. */ @@ -170,7 +169,7 @@ declare module DebugProtocol { /** Event message for 'exited' event type. The event indicates that the debuggee has exited and returns its exit code. */ - export interface ExitedEvent extends Event { + interface ExitedEvent extends Event { // event: 'exited'; body: { /** The exit code returned from the debuggee. */ @@ -181,7 +180,7 @@ declare module DebugProtocol { /** Event message for 'terminated' event type. The event indicates that debugging of the debuggee has terminated. This does **not** mean that the debuggee itself has exited. */ - export interface TerminatedEvent extends Event { + interface TerminatedEvent extends Event { // event: 'terminated'; body?: { /** A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session. @@ -194,7 +193,7 @@ declare module DebugProtocol { /** Event message for 'thread' event type. The event indicates that a thread has started or exited. */ - export interface ThreadEvent extends Event { + interface ThreadEvent extends Event { // event: 'thread'; body: { /** The reason for the event. @@ -209,7 +208,7 @@ declare module DebugProtocol { /** Event message for 'output' event type. The event indicates that the target has produced some output. */ - export interface OutputEvent extends Event { + interface OutputEvent extends Event { // event: 'output'; body: { /** The output category. If not specified or if the category is not understand by the client, 'console' is assumed. @@ -249,7 +248,7 @@ declare module DebugProtocol { /** Event message for 'breakpoint' event type. The event indicates that some information about a breakpoint has changed. */ - export interface BreakpointEvent extends Event { + interface BreakpointEvent extends Event { // event: 'breakpoint'; body: { /** The reason for the event. @@ -264,7 +263,7 @@ declare module DebugProtocol { /** Event message for 'module' event type. The event indicates that some information about a module has changed. */ - export interface ModuleEvent extends Event { + interface ModuleEvent extends Event { // event: 'module'; body: { /** The reason for the event. */ @@ -277,7 +276,7 @@ declare module DebugProtocol { /** Event message for 'loadedSource' event type. The event indicates that some source has been added, changed, or removed from the set of all loaded sources. */ - export interface LoadedSourceEvent extends Event { + interface LoadedSourceEvent extends Event { // event: 'loadedSource'; body: { /** The reason for the event. */ @@ -290,7 +289,7 @@ declare module DebugProtocol { /** Event message for 'process' event type. The event indicates that the debugger has begun debugging a new process. Either one that it has launched, or one that it has attached to. */ - export interface ProcessEvent extends Event { + interface ProcessEvent extends Event { // event: 'process'; body: { /** The logical name of the process. This is usually the full path to process's executable file. Example: /home/example/myproj/program.js. */ @@ -316,7 +315,7 @@ declare module DebugProtocol { Consequently this event has a hint characteristic: a frontend can only be expected to make a 'best effort' in honouring individual capabilities but there are no guarantees. Only changed capabilities need to be included, all other capabilities keep their values. */ - export interface CapabilitiesEvent extends Event { + interface CapabilitiesEvent extends Event { // event: 'capabilities'; body: { /** The set of updated capabilities. */ @@ -330,7 +329,7 @@ declare module DebugProtocol { The client is free to delay the showing of the UI in order to reduce flicker. This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ - export interface ProgressStartEvent extends Event { + interface ProgressStartEvent extends Event { // event: 'progressStart'; body: { /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. @@ -361,7 +360,7 @@ declare module DebugProtocol { The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ - export interface ProgressUpdateEvent extends Event { + interface ProgressUpdateEvent extends Event { // event: 'progressUpdate'; body: { /** The ID that was introduced in the initial 'progressStart' event. */ @@ -377,7 +376,7 @@ declare module DebugProtocol { The event signals the end of the progress reporting with an optional final message. This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ - export interface ProgressEndEvent extends Event { + interface ProgressEndEvent extends Event { // event: 'progressEnd'; body: { /** The ID that was introduced in the initial 'ProgressStartEvent'. */ @@ -392,7 +391,7 @@ declare module DebugProtocol { Debug adapters do not have to emit this event for runtime changes like stopped or thread events because in that case the client refetches the new state anyway. But the event can be used for example to refresh the UI after rendering formatting has changed in the debug adapter. This event should only be sent if the debug adapter has received a value true for the 'supportsInvalidatedEvent' capability of the 'initialize' request. */ - export interface InvalidatedEvent extends Event { + interface InvalidatedEvent extends Event { // event: 'invalidated'; body: { /** Optional set of logical areas that got invalidated. This property has a hint characteristic: a client can only be expected to make a 'best effort' in honouring the areas but there are no guarantees. If this property is missing, empty, or if values are not understand the client should assume a single value 'all'. */ @@ -409,7 +408,7 @@ declare module DebugProtocol { Clients typically react to the event by re-issuing a `readMemory` request if they show the memory identified by the `memoryReference` and if the updated memory range overlaps the displayed range. Clients should not make assumptions how individual memory references relate to each other, so they should not assume that they are part of a single continuous address range and might overlap. Debug adapters can use this event to indicate that the contents of a memory range has changed due to some other DAP request like `setVariable` or `setExpression`. Debug adapters are not expected to emit this event for each and every memory change of a running program, because that information is typically not available from debuggers and it would flood clients with too many events. */ - export interface MemoryEvent extends Event { + interface MemoryEvent extends Event { // event: 'memory'; body: { /** Memory reference of a memory range that has been updated. */ @@ -426,13 +425,13 @@ declare module DebugProtocol { This is typically used to launch the debuggee in a terminal provided by the client. This request should only be called if the client has passed the value true for the 'supportsRunInTerminalRequest' capability of the 'initialize' request. */ - export interface RunInTerminalRequest extends Request { + interface RunInTerminalRequest extends Request { // command: 'runInTerminal'; arguments: RunInTerminalRequestArguments; } /** Arguments for 'runInTerminal' request. */ - export interface RunInTerminalRequestArguments { + interface RunInTerminalRequestArguments { /** What kind of terminal to launch. */ kind?: 'integrated' | 'external'; /** Optional title of the terminal. */ @@ -446,7 +445,7 @@ declare module DebugProtocol { } /** Response to 'runInTerminal' request. */ - export interface RunInTerminalResponse extends Response { + interface RunInTerminalResponse extends Response { body: { /** The process ID. The value should be less than or equal to 2147483647 (2^31-1). */ processId?: number; @@ -462,13 +461,13 @@ declare module DebugProtocol { In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. The 'initialize' request may only be sent once. */ - export interface InitializeRequest extends Request { + interface InitializeRequest extends Request { // command: 'initialize'; arguments: InitializeRequestArguments; } /** Arguments for 'initialize' request. */ - export interface InitializeRequestArguments { + interface InitializeRequestArguments { /** The ID of the (frontend) client using this adapter. */ clientID?: string; /** The human readable name of the (frontend) client using this adapter. */ @@ -502,7 +501,7 @@ declare module DebugProtocol { } /** Response to 'initialize' request. */ - export interface InitializeResponse extends Response { + interface InitializeResponse extends Response { /** The capabilities of this debug adapter. */ body?: Capabilities; } @@ -512,30 +511,30 @@ declare module DebugProtocol { So it is the last request in the sequence of configuration requests (which was started by the 'initialized' event). Clients should only call this request if the capability 'supportsConfigurationDoneRequest' is true. */ - export interface ConfigurationDoneRequest extends Request { + interface ConfigurationDoneRequest extends Request { // command: 'configurationDone'; arguments?: ConfigurationDoneArguments; } /** Arguments for 'configurationDone' request. */ - export interface ConfigurationDoneArguments { + interface ConfigurationDoneArguments { } /** Response to 'configurationDone' request. This is just an acknowledgement, so no body field is required. */ - export interface ConfigurationDoneResponse extends Response { + interface ConfigurationDoneResponse extends Response { } /** Launch request; value of command field is 'launch'. This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. */ - export interface LaunchRequest extends Request { + interface LaunchRequest extends Request { // command: 'launch'; arguments: LaunchRequestArguments; } /** Arguments for 'launch' request. Additional attributes are implementation specific. */ - export interface LaunchRequestArguments { + interface LaunchRequestArguments { /** If noDebug is true the launch request should launch the program without enabling debugging. */ noDebug?: boolean; /** Optional data from the previous, restarted session. @@ -546,20 +545,20 @@ declare module DebugProtocol { } /** Response to 'launch' request. This is just an acknowledgement, so no body field is required. */ - export interface LaunchResponse extends Response { + interface LaunchResponse extends Response { } /** Attach request; value of command field is 'attach'. The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. */ - export interface AttachRequest extends Request { + interface AttachRequest extends Request { // command: 'attach'; arguments: AttachRequestArguments; } /** Arguments for 'attach' request. Additional attributes are implementation specific. */ - export interface AttachRequestArguments { + interface AttachRequestArguments { /** Optional data from the previous, restarted session. The data is sent as the 'restart' attribute of the 'terminated' event. The client should leave the data intact. @@ -568,26 +567,26 @@ declare module DebugProtocol { } /** Response to 'attach' request. This is just an acknowledgement, so no body field is required. */ - export interface AttachResponse extends Response { + interface AttachResponse extends Response { } /** Restart request; value of command field is 'restart'. Restarts a debug session. Clients should only call this request if the capability 'supportsRestartRequest' is true. If the capability is missing or has the value false, a typical client will emulate 'restart' by terminating the debug adapter first and then launching it anew. */ - export interface RestartRequest extends Request { + interface RestartRequest extends Request { // command: 'restart'; arguments?: RestartArguments; } /** Arguments for 'restart' request. */ - export interface RestartArguments { + interface RestartArguments { /** The latest version of the 'launch' or 'attach' configuration. */ arguments?: LaunchRequestArguments | AttachRequestArguments; } /** Response to 'restart' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartResponse extends Response { + interface RestartResponse extends Response { } /** Disconnect request; value of command field is 'disconnect'. @@ -597,13 +596,13 @@ declare module DebugProtocol { If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). */ - export interface DisconnectRequest extends Request { + interface DisconnectRequest extends Request { // command: 'disconnect'; arguments?: DisconnectArguments; } /** Arguments for 'disconnect' request. */ - export interface DisconnectArguments { + interface DisconnectArguments { /** A value of true indicates that this 'disconnect' request is part of a restart sequence. */ restart?: boolean; /** Indicates whether the debuggee should be terminated when the debugger is disconnected. @@ -619,39 +618,39 @@ declare module DebugProtocol { } /** Response to 'disconnect' request. This is just an acknowledgement, so no body field is required. */ - export interface DisconnectResponse extends Response { + interface DisconnectResponse extends Response { } /** Terminate request; value of command field is 'terminate'. The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. Clients should only call this request if the capability 'supportsTerminateRequest' is true. */ - export interface TerminateRequest extends Request { + interface TerminateRequest extends Request { // command: 'terminate'; arguments?: TerminateArguments; } /** Arguments for 'terminate' request. */ - export interface TerminateArguments { + interface TerminateArguments { /** A value of true indicates that this 'terminate' request is part of a restart sequence. */ restart?: boolean; } /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateResponse extends Response { + interface TerminateResponse extends Response { } /** BreakpointLocations request; value of command field is 'breakpointLocations'. The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. Clients should only call this request if the capability 'supportsBreakpointLocationsRequest' is true. */ - export interface BreakpointLocationsRequest extends Request { + interface BreakpointLocationsRequest extends Request { // command: 'breakpointLocations'; arguments?: BreakpointLocationsArguments; } /** Arguments for 'breakpointLocations' request. */ - export interface BreakpointLocationsArguments { + interface BreakpointLocationsArguments { /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ source: Source; /** Start line of range to search possible breakpoint locations in. If only the line is specified, the request returns all possible locations in that line. */ @@ -667,7 +666,7 @@ declare module DebugProtocol { /** Response to 'breakpointLocations' request. Contains possible locations for source breakpoints. */ - export interface BreakpointLocationsResponse extends Response { + interface BreakpointLocationsResponse extends Response { body: { /** Sorted set of possible breakpoint locations. */ breakpoints: BreakpointLocation[]; @@ -679,13 +678,13 @@ declare module DebugProtocol { To clear all breakpoint for a source, specify an empty array. When a breakpoint is hit, a 'stopped' event (with reason 'breakpoint') is generated. */ - export interface SetBreakpointsRequest extends Request { + interface SetBreakpointsRequest extends Request { // command: 'setBreakpoints'; arguments: SetBreakpointsArguments; } /** Arguments for 'setBreakpoints' request. */ - export interface SetBreakpointsArguments { + interface SetBreakpointsArguments { /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ source: Source; /** The code locations of the breakpoints. */ @@ -702,7 +701,7 @@ declare module DebugProtocol { The breakpoints returned are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ - export interface SetBreakpointsResponse extends Response { + interface SetBreakpointsResponse extends Response { body: { /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. @@ -717,13 +716,13 @@ declare module DebugProtocol { When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. Clients should only call this request if the capability 'supportsFunctionBreakpoints' is true. */ - export interface SetFunctionBreakpointsRequest extends Request { + interface SetFunctionBreakpointsRequest extends Request { // command: 'setFunctionBreakpoints'; arguments: SetFunctionBreakpointsArguments; } /** Arguments for 'setFunctionBreakpoints' request. */ - export interface SetFunctionBreakpointsArguments { + interface SetFunctionBreakpointsArguments { /** The function names of the breakpoints. */ breakpoints: FunctionBreakpoint[]; } @@ -731,7 +730,7 @@ declare module DebugProtocol { /** Response to 'setFunctionBreakpoints' request. Returned is information about each breakpoint created by this request. */ - export interface SetFunctionBreakpointsResponse extends Response { + interface SetFunctionBreakpointsResponse extends Response { body: { /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ breakpoints: Breakpoint[]; @@ -743,13 +742,13 @@ declare module DebugProtocol { If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters. */ - export interface SetExceptionBreakpointsRequest extends Request { + interface SetExceptionBreakpointsRequest extends Request { // command: 'setExceptionBreakpoints'; arguments: SetExceptionBreakpointsArguments; } /** Arguments for 'setExceptionBreakpoints' request. */ - export interface SetExceptionBreakpointsArguments { + interface SetExceptionBreakpointsArguments { /** Set of exception filters specified by their ID. The set of all possible exception filters is defined by the 'exceptionBreakpointFilters' capability. The 'filter' and 'filterOptions' sets are additive. */ filters: string[]; /** Set of exception filters and their options. The set of all possible exception filters is defined by the 'exceptionBreakpointFilters' capability. This attribute is only honored by a debug adapter if the capability 'supportsExceptionFilterOptions' is true. The 'filter' and 'filterOptions' sets are additive. */ @@ -765,7 +764,7 @@ declare module DebugProtocol { The mandatory 'verified' property of a Breakpoint object signals whether the exception breakpoint or filter could be successfully created and whether the optional condition or hit count expressions are valid. In case of an error the 'message' property explains the problem. An optional 'id' property can be used to introduce a unique ID for the exception breakpoint or filter so that it can be updated subsequently by sending breakpoint events. For backward compatibility both the 'breakpoints' array and the enclosing 'body' are optional. If these elements are missing a client will not be able to show problems for individual exception breakpoints or filters. */ - export interface SetExceptionBreakpointsResponse extends Response { + interface SetExceptionBreakpointsResponse extends Response { body?: { /** Information about the exception breakpoints or filters. The breakpoints returned are in the same order as the elements of the 'filters', 'filterOptions', 'exceptionOptions' arrays in the arguments. If both 'filters' and 'filterOptions' are given, the returned array must start with 'filters' information first, followed by 'filterOptions' information. @@ -778,13 +777,13 @@ declare module DebugProtocol { Obtains information on a possible data breakpoint that could be set on an expression or variable. Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ - export interface DataBreakpointInfoRequest extends Request { + interface DataBreakpointInfoRequest extends Request { // command: 'dataBreakpointInfo'; arguments: DataBreakpointInfoArguments; } /** Arguments for 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoArguments { + interface DataBreakpointInfoArguments { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; /** The name of the Variable's child to obtain data breakpoint information for. @@ -794,7 +793,7 @@ declare module DebugProtocol { } /** Response to 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoResponse extends Response { + interface DataBreakpointInfoResponse extends Response { body: { /** An identifier for the data on which a data breakpoint can be registered with the setDataBreakpoints request or null if no data breakpoint is available. */ dataId: string | null; @@ -813,13 +812,13 @@ declare module DebugProtocol { When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ - export interface SetDataBreakpointsRequest extends Request { + interface SetDataBreakpointsRequest extends Request { // command: 'setDataBreakpoints'; arguments: SetDataBreakpointsArguments; } /** Arguments for 'setDataBreakpoints' request. */ - export interface SetDataBreakpointsArguments { + interface SetDataBreakpointsArguments { /** The contents of this array replaces all existing data breakpoints. An empty array clears all data breakpoints. */ breakpoints: DataBreakpoint[]; } @@ -827,7 +826,7 @@ declare module DebugProtocol { /** Response to 'setDataBreakpoints' request. Returned is information about each breakpoint created by this request. */ - export interface SetDataBreakpointsResponse extends Response { + interface SetDataBreakpointsResponse extends Response { body: { /** Information about the data breakpoints. The array elements correspond to the elements of the input argument 'breakpoints' array. */ breakpoints: Breakpoint[]; @@ -840,19 +839,19 @@ declare module DebugProtocol { When an instruction breakpoint is hit, a 'stopped' event (with reason 'instruction breakpoint') is generated. Clients should only call this request if the capability 'supportsInstructionBreakpoints' is true. */ - export interface SetInstructionBreakpointsRequest extends Request { + interface SetInstructionBreakpointsRequest extends Request { // command: 'setInstructionBreakpoints'; arguments: SetInstructionBreakpointsArguments; } /** Arguments for 'setInstructionBreakpoints' request */ - export interface SetInstructionBreakpointsArguments { + interface SetInstructionBreakpointsArguments { /** The instruction references of the breakpoints */ breakpoints: InstructionBreakpoint[]; } /** Response to 'setInstructionBreakpoints' request */ - export interface SetInstructionBreakpointsResponse extends Response { + interface SetInstructionBreakpointsResponse extends Response { body: { /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ breakpoints: Breakpoint[]; @@ -862,13 +861,13 @@ declare module DebugProtocol { /** Continue request; value of command field is 'continue'. The request resumes execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. */ - export interface ContinueRequest extends Request { + interface ContinueRequest extends Request { // command: 'continue'; arguments: ContinueArguments; } /** Arguments for 'continue' request. */ - export interface ContinueArguments { + interface ContinueArguments { /** Specifies the active thread. If the debug adapter supports single thread execution (see 'supportsSingleThreadExecutionRequests') and the optional argument 'singleThread' is true, only the thread with this ID is resumed. */ threadId: number; /** If this optional flag is true, execution is resumed only for the thread with given 'threadId'. */ @@ -876,7 +875,7 @@ declare module DebugProtocol { } /** Response to 'continue' request. */ - export interface ContinueResponse extends Response { + interface ContinueResponse extends Response { body: { /** The value true (or a missing property) signals to the client that all threads have been resumed. The value false must be returned if not all threads were resumed. */ allThreadsContinued?: boolean; @@ -888,13 +887,13 @@ declare module DebugProtocol { If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ - export interface NextRequest extends Request { + interface NextRequest extends Request { // command: 'next'; arguments: NextArguments; } /** Arguments for 'next' request. */ - export interface NextArguments { + interface NextArguments { /** Specifies the thread for which to resume execution for one step (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ @@ -904,7 +903,7 @@ declare module DebugProtocol { } /** Response to 'next' request. This is just an acknowledgement, so no body field is required. */ - export interface NextResponse extends Response { + interface NextResponse extends Response { } /** StepIn request; value of command field is 'stepIn'. @@ -916,13 +915,13 @@ declare module DebugProtocol { the optional argument 'targetId' can be used to control into which target the 'stepIn' should occur. The list of possible targets for a given source line can be retrieved via the 'stepInTargets' request. */ - export interface StepInRequest extends Request { + interface StepInRequest extends Request { // command: 'stepIn'; arguments: StepInArguments; } /** Arguments for 'stepIn' request. */ - export interface StepInArguments { + interface StepInArguments { /** Specifies the thread for which to resume execution for one step-into (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ @@ -934,7 +933,7 @@ declare module DebugProtocol { } /** Response to 'stepIn' request. This is just an acknowledgement, so no body field is required. */ - export interface StepInResponse extends Response { + interface StepInResponse extends Response { } /** StepOut request; value of command field is 'stepOut'. @@ -942,13 +941,13 @@ declare module DebugProtocol { If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true prevents other suspended threads from resuming. The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. */ - export interface StepOutRequest extends Request { + interface StepOutRequest extends Request { // command: 'stepOut'; arguments: StepOutArguments; } /** Arguments for 'stepOut' request. */ - export interface StepOutArguments { + interface StepOutArguments { /** Specifies the thread for which to resume execution for one step-out (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ @@ -958,7 +957,7 @@ declare module DebugProtocol { } /** Response to 'stepOut' request. This is just an acknowledgement, so no body field is required. */ - export interface StepOutResponse extends Response { + interface StepOutResponse extends Response { } /** StepBack request; value of command field is 'stepBack'. @@ -967,13 +966,13 @@ declare module DebugProtocol { The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. */ - export interface StepBackRequest extends Request { + interface StepBackRequest extends Request { // command: 'stepBack'; arguments: StepBackArguments; } /** Arguments for 'stepBack' request. */ - export interface StepBackArguments { + interface StepBackArguments { /** Specifies the thread for which to resume execution for one step backwards (of the given granularity). */ threadId: number; /** If this optional flag is true, all other suspended threads are not resumed. */ @@ -983,20 +982,20 @@ declare module DebugProtocol { } /** Response to 'stepBack' request. This is just an acknowledgement, so no body field is required. */ - export interface StepBackResponse extends Response { + interface StepBackResponse extends Response { } /** ReverseContinue request; value of command field is 'reverseContinue'. The request resumes backward execution of all threads. If the debug adapter supports single thread execution (see capability 'supportsSingleThreadExecutionRequests') setting the 'singleThread' argument to true resumes only the specified thread. If not all threads were resumed, the 'allThreadsContinued' attribute of the response must be set to false. Clients should only call this request if the capability 'supportsStepBack' is true. */ - export interface ReverseContinueRequest extends Request { + interface ReverseContinueRequest extends Request { // command: 'reverseContinue'; arguments: ReverseContinueArguments; } /** Arguments for 'reverseContinue' request. */ - export interface ReverseContinueArguments { + interface ReverseContinueArguments { /** Specifies the active thread. If the debug adapter supports single thread execution (see 'supportsSingleThreadExecutionRequests') and the optional argument 'singleThread' is true, only the thread with this ID is resumed. */ threadId: number; /** If this optional flag is true, backward execution is resumed only for the thread with given 'threadId'. */ @@ -1004,7 +1003,7 @@ declare module DebugProtocol { } /** Response to 'reverseContinue' request. This is just an acknowledgement, so no body field is required. */ - export interface ReverseContinueResponse extends Response { + interface ReverseContinueResponse extends Response { } /** RestartFrame request; value of command field is 'restartFrame'. @@ -1012,19 +1011,19 @@ declare module DebugProtocol { The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. Clients should only call this request if the capability 'supportsRestartFrame' is true. */ - export interface RestartFrameRequest extends Request { + interface RestartFrameRequest extends Request { // command: 'restartFrame'; arguments: RestartFrameArguments; } /** Arguments for 'restartFrame' request. */ - export interface RestartFrameArguments { + interface RestartFrameArguments { /** Restart this stackframe. */ frameId: number; } /** Response to 'restartFrame' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartFrameResponse extends Response { + interface RestartFrameResponse extends Response { } /** Goto request; value of command field is 'goto'. @@ -1034,13 +1033,13 @@ declare module DebugProtocol { The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true (because only then goto targets exist that can be passed as arguments). */ - export interface GotoRequest extends Request { + interface GotoRequest extends Request { // command: 'goto'; arguments: GotoArguments; } /** Arguments for 'goto' request. */ - export interface GotoArguments { + interface GotoArguments { /** Set the goto target for this thread. */ threadId: number; /** The location where the debuggee will continue to run. */ @@ -1048,39 +1047,39 @@ declare module DebugProtocol { } /** Response to 'goto' request. This is just an acknowledgement, so no body field is required. */ - export interface GotoResponse extends Response { + interface GotoResponse extends Response { } /** Pause request; value of command field is 'pause'. The request suspends the debuggee. The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. */ - export interface PauseRequest extends Request { + interface PauseRequest extends Request { // command: 'pause'; arguments: PauseArguments; } /** Arguments for 'pause' request. */ - export interface PauseArguments { + interface PauseArguments { /** Pause execution for this thread. */ threadId: number; } /** Response to 'pause' request. This is just an acknowledgement, so no body field is required. */ - export interface PauseResponse extends Response { + interface PauseResponse extends Response { } /** StackTrace request; value of command field is 'stackTrace'. The request returns a stacktrace from the current execution state of a given thread. A client can request all stack frames by omitting the startFrame and levels arguments. For performance conscious clients and if the debug adapter's 'supportsDelayedStackTraceLoading' capability is true, stack frames can be retrieved in a piecemeal way with the startFrame and levels arguments. The response of the stackTrace request may contain a totalFrames property that hints at the total number of frames in the stack. If a client needs this total number upfront, it can issue a request for a single (first) frame and depending on the value of totalFrames decide how to proceed. In any case a client should be prepared to receive less frames than requested, which is an indication that the end of the stack has been reached. */ - export interface StackTraceRequest extends Request { + interface StackTraceRequest extends Request { // command: 'stackTrace'; arguments: StackTraceArguments; } /** Arguments for 'stackTrace' request. */ - export interface StackTraceArguments { + interface StackTraceArguments { /** Retrieve the stacktrace for this thread. */ threadId: number; /** The index of the first frame to return; if omitted frames start at 0. */ @@ -1094,7 +1093,7 @@ declare module DebugProtocol { } /** Response to 'stackTrace' request. */ - export interface StackTraceResponse extends Response { + interface StackTraceResponse extends Response { body: { /** The frames of the stackframe. If the array has length zero, there are no stackframes available. This means that there is no location information available. @@ -1108,19 +1107,19 @@ declare module DebugProtocol { /** Scopes request; value of command field is 'scopes'. The request returns the variable scopes for a given stackframe ID. */ - export interface ScopesRequest extends Request { + interface ScopesRequest extends Request { // command: 'scopes'; arguments: ScopesArguments; } /** Arguments for 'scopes' request. */ - export interface ScopesArguments { + interface ScopesArguments { /** Retrieve the scopes for this stackframe. */ frameId: number; } /** Response to 'scopes' request. */ - export interface ScopesResponse extends Response { + interface ScopesResponse extends Response { body: { /** The scopes of the stackframe. If the array has length zero, there are no scopes available. */ scopes: Scope[]; @@ -1131,13 +1130,13 @@ declare module DebugProtocol { Retrieves all child variables for the given variable reference. An optional filter can be used to limit the fetched children to either named or indexed children. */ - export interface VariablesRequest extends Request { + interface VariablesRequest extends Request { // command: 'variables'; arguments: VariablesArguments; } /** Arguments for 'variables' request. */ - export interface VariablesArguments { + interface VariablesArguments { /** The Variable reference. */ variablesReference: number; /** Optional filter to limit the child variables to either named or indexed. If omitted, both types are fetched. */ @@ -1153,7 +1152,7 @@ declare module DebugProtocol { } /** Response to 'variables' request. */ - export interface VariablesResponse extends Response { + interface VariablesResponse extends Response { body: { /** All (or a range) of variables for the given variable reference. */ variables: Variable[]; @@ -1164,13 +1163,13 @@ declare module DebugProtocol { Set the variable with the given name in the variable container to a new value. Clients should only call this request if the capability 'supportsSetVariable' is true. If a debug adapter implements both setVariable and setExpression, a client will only use setExpression if the variable has an evaluateName property. */ - export interface SetVariableRequest extends Request { + interface SetVariableRequest extends Request { // command: 'setVariable'; arguments: SetVariableArguments; } /** Arguments for 'setVariable' request. */ - export interface SetVariableArguments { + interface SetVariableArguments { /** The reference of the variable container. */ variablesReference: number; /** The name of the variable in the container. */ @@ -1182,7 +1181,7 @@ declare module DebugProtocol { } /** Response to 'setVariable' request. */ - export interface SetVariableResponse extends Response { + interface SetVariableResponse extends Response { body: { /** The new value of the variable. */ value: string; @@ -1208,13 +1207,13 @@ declare module DebugProtocol { /** Source request; value of command field is 'source'. The request retrieves the source code for a given source reference. */ - export interface SourceRequest extends Request { + interface SourceRequest extends Request { // command: 'source'; arguments: SourceArguments; } /** Arguments for 'source' request. */ - export interface SourceArguments { + interface SourceArguments { /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ source?: Source; /** The reference to the source. This is the same as source.sourceReference. @@ -1224,7 +1223,7 @@ declare module DebugProtocol { } /** Response to 'source' request. */ - export interface SourceResponse extends Response { + interface SourceResponse extends Response { body: { /** Content of the source reference. */ content: string; @@ -1236,12 +1235,12 @@ declare module DebugProtocol { /** Threads request; value of command field is 'threads'. The request retrieves a list of all threads. */ - export interface ThreadsRequest extends Request { + interface ThreadsRequest extends Request { // command: 'threads'; } /** Response to 'threads' request. */ - export interface ThreadsResponse extends Response { + interface ThreadsResponse extends Response { body: { /** All threads. */ threads: Thread[]; @@ -1252,32 +1251,32 @@ declare module DebugProtocol { The request terminates the threads with the given ids. Clients should only call this request if the capability 'supportsTerminateThreadsRequest' is true. */ - export interface TerminateThreadsRequest extends Request { + interface TerminateThreadsRequest extends Request { // command: 'terminateThreads'; arguments: TerminateThreadsArguments; } /** Arguments for 'terminateThreads' request. */ - export interface TerminateThreadsArguments { + interface TerminateThreadsArguments { /** Ids of threads to be terminated. */ threadIds?: number[]; } /** Response to 'terminateThreads' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateThreadsResponse extends Response { + interface TerminateThreadsResponse extends Response { } /** Modules request; value of command field is 'modules'. Modules can be retrieved from the debug adapter with this request which can either return all modules or a range of modules to support paging. Clients should only call this request if the capability 'supportsModulesRequest' is true. */ - export interface ModulesRequest extends Request { + interface ModulesRequest extends Request { // command: 'modules'; arguments: ModulesArguments; } /** Arguments for 'modules' request. */ - export interface ModulesArguments { + interface ModulesArguments { /** The index of the first module to return; if omitted modules start at 0. */ startModule?: number; /** The number of modules to return. If moduleCount is not specified or 0, all modules are returned. */ @@ -1285,7 +1284,7 @@ declare module DebugProtocol { } /** Response to 'modules' request. */ - export interface ModulesResponse extends Response { + interface ModulesResponse extends Response { body: { /** All modules or range of modules. */ modules: Module[]; @@ -1298,17 +1297,17 @@ declare module DebugProtocol { Retrieves the set of all sources currently loaded by the debugged process. Clients should only call this request if the capability 'supportsLoadedSourcesRequest' is true. */ - export interface LoadedSourcesRequest extends Request { + interface LoadedSourcesRequest extends Request { // command: 'loadedSources'; arguments?: LoadedSourcesArguments; } /** Arguments for 'loadedSources' request. */ - export interface LoadedSourcesArguments { + interface LoadedSourcesArguments { } /** Response to 'loadedSources' request. */ - export interface LoadedSourcesResponse extends Response { + interface LoadedSourcesResponse extends Response { body: { /** Set of loaded sources. */ sources: Source[]; @@ -1319,35 +1318,37 @@ declare module DebugProtocol { Evaluates the given expression in the context of the top most stack frame. The expression has access to any variables and arguments that are in scope. */ - export interface EvaluateRequest extends Request { + interface EvaluateRequest extends Request { // command: 'evaluate'; arguments: EvaluateArguments; } /** Arguments for 'evaluate' request. */ - export interface EvaluateArguments { + interface EvaluateArguments { /** The expression to evaluate. */ expression: string; /** Evaluate the expression in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. */ frameId?: number; - /** The context in which the evaluate request is run. + /** The context in which the evaluate request is used. Values: - 'watch': evaluate is run in a watch. - 'repl': evaluate is run from REPL console. - 'hover': evaluate is run from a data hover. - 'clipboard': evaluate is run to generate the value that will be stored in the clipboard. - The attribute is only honored by a debug adapter if the capability 'supportsClipboardContext' is true. + 'variables': evaluate is called from a variables view context. + 'watch': evaluate is called from a watch view context. + 'repl': evaluate is called from a REPL context. + 'hover': evaluate is called to generate the debug hover contents. + This value should only be used if the capability 'supportsEvaluateForHovers' is true. + 'clipboard': evaluate is called to generate clipboard contents. + This value should only be used if the capability 'supportsClipboardContext' is true. etc. */ - context?: 'watch' | 'repl' | 'hover' | 'clipboard' | string; - /** Specifies details on how to format the Evaluate result. + context?: 'variables' | 'watch' | 'repl' | 'hover' | 'clipboard' | string; + /** Specifies details on how to format the result. The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. */ format?: ValueFormat; } /** Response to 'evaluate' request. */ - export interface EvaluateResponse extends Response { + interface EvaluateResponse extends Response { body: { /** The result of the evaluate request. */ result: string; @@ -1385,13 +1386,13 @@ declare module DebugProtocol { Clients should only call this request if the capability 'supportsSetExpression' is true. If a debug adapter implements both setExpression and setVariable, a client will only use setExpression if the variable has an evaluateName property. */ - export interface SetExpressionRequest extends Request { + interface SetExpressionRequest extends Request { // command: 'setExpression'; arguments: SetExpressionArguments; } /** Arguments for 'setExpression' request. */ - export interface SetExpressionArguments { + interface SetExpressionArguments { /** The l-value expression to assign to. */ expression: string; /** The value expression to assign to the l-value expression. */ @@ -1403,7 +1404,7 @@ declare module DebugProtocol { } /** Response to 'setExpression' request. */ - export interface SetExpressionResponse extends Response { + interface SetExpressionResponse extends Response { body: { /** The new value of the expression. */ value: string; @@ -1436,19 +1437,19 @@ declare module DebugProtocol { The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. Clients should only call this request if the capability 'supportsStepInTargetsRequest' is true. */ - export interface StepInTargetsRequest extends Request { + interface StepInTargetsRequest extends Request { // command: 'stepInTargets'; arguments: StepInTargetsArguments; } /** Arguments for 'stepInTargets' request. */ - export interface StepInTargetsArguments { + interface StepInTargetsArguments { /** The stack frame for which to retrieve the possible stepIn targets. */ frameId: number; } /** Response to 'stepInTargets' request. */ - export interface StepInTargetsResponse extends Response { + interface StepInTargetsResponse extends Response { body: { /** The possible stepIn targets of the specified source location. */ targets: StepInTarget[]; @@ -1460,13 +1461,13 @@ declare module DebugProtocol { These targets can be used in the 'goto' request. Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true. */ - export interface GotoTargetsRequest extends Request { + interface GotoTargetsRequest extends Request { // command: 'gotoTargets'; arguments: GotoTargetsArguments; } /** Arguments for 'gotoTargets' request. */ - export interface GotoTargetsArguments { + interface GotoTargetsArguments { /** The source location for which the goto targets are determined. */ source: Source; /** The line location for which the goto targets are determined. */ @@ -1476,7 +1477,7 @@ declare module DebugProtocol { } /** Response to 'gotoTargets' request. */ - export interface GotoTargetsResponse extends Response { + interface GotoTargetsResponse extends Response { body: { /** The possible goto targets of the specified location. */ targets: GotoTarget[]; @@ -1487,13 +1488,13 @@ declare module DebugProtocol { Returns a list of possible completions for a given caret position and text. Clients should only call this request if the capability 'supportsCompletionsRequest' is true. */ - export interface CompletionsRequest extends Request { + interface CompletionsRequest extends Request { // command: 'completions'; arguments: CompletionsArguments; } /** Arguments for 'completions' request. */ - export interface CompletionsArguments { + interface CompletionsArguments { /** Returns completions in the scope of this stack frame. If not specified, the completions are returned for the global scope. */ frameId?: number; /** One or more source lines. Typically this is the text a user has typed into the debug console before he asked for completion. */ @@ -1505,7 +1506,7 @@ declare module DebugProtocol { } /** Response to 'completions' request. */ - export interface CompletionsResponse extends Response { + interface CompletionsResponse extends Response { body: { /** The possible completions for . */ targets: CompletionItem[]; @@ -1516,19 +1517,19 @@ declare module DebugProtocol { Retrieves the details of the exception that caused this event to be raised. Clients should only call this request if the capability 'supportsExceptionInfoRequest' is true. */ - export interface ExceptionInfoRequest extends Request { + interface ExceptionInfoRequest extends Request { // command: 'exceptionInfo'; arguments: ExceptionInfoArguments; } /** Arguments for 'exceptionInfo' request. */ - export interface ExceptionInfoArguments { + interface ExceptionInfoArguments { /** Thread for which exception information should be retrieved. */ threadId: number; } /** Response to 'exceptionInfo' request. */ - export interface ExceptionInfoResponse extends Response { + interface ExceptionInfoResponse extends Response { body: { /** ID of the exception that was thrown. */ exceptionId: string; @@ -1545,13 +1546,13 @@ declare module DebugProtocol { Reads bytes from memory at the provided location. Clients should only call this request if the capability 'supportsReadMemoryRequest' is true. */ - export interface ReadMemoryRequest extends Request { + interface ReadMemoryRequest extends Request { // command: 'readMemory'; arguments: ReadMemoryArguments; } /** Arguments for 'readMemory' request. */ - export interface ReadMemoryArguments { + interface ReadMemoryArguments { /** Memory reference to the base location from which data should be read. */ memoryReference: string; /** Optional offset (in bytes) to be applied to the reference location before reading data. Can be negative. */ @@ -1561,7 +1562,7 @@ declare module DebugProtocol { } /** Response to 'readMemory' request. */ - export interface ReadMemoryResponse extends Response { + interface ReadMemoryResponse extends Response { body?: { /** The address of the first byte of data returned. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. @@ -1580,13 +1581,13 @@ declare module DebugProtocol { Writes bytes to memory at the provided location. Clients should only call this request if the capability 'supportsWriteMemoryRequest' is true. */ - export interface WriteMemoryRequest extends Request { + interface WriteMemoryRequest extends Request { // command: 'writeMemory'; arguments: WriteMemoryArguments; } /** Arguments for 'writeMemory' request. */ - export interface WriteMemoryArguments { + interface WriteMemoryArguments { /** Memory reference to the base location to which data should be written. */ memoryReference: string; /** Optional offset (in bytes) to be applied to the reference location before writing data. Can be negative. */ @@ -1600,7 +1601,7 @@ declare module DebugProtocol { } /** Response to 'writeMemory' request. */ - export interface WriteMemoryResponse extends Response { + interface WriteMemoryResponse extends Response { body?: { /** Optional property that should be returned when 'allowPartial' is true to indicate the offset of the first byte of data successfully written. Can be negative. */ offset?: number; @@ -1613,13 +1614,13 @@ declare module DebugProtocol { Disassembles code stored at the provided location. Clients should only call this request if the capability 'supportsDisassembleRequest' is true. */ - export interface DisassembleRequest extends Request { + interface DisassembleRequest extends Request { // command: 'disassemble'; arguments: DisassembleArguments; } /** Arguments for 'disassemble' request. */ - export interface DisassembleArguments { + interface DisassembleArguments { /** Memory reference to the base location containing the instructions to disassemble. */ memoryReference: string; /** Optional offset (in bytes) to be applied to the reference location before disassembling. Can be negative. */ @@ -1635,7 +1636,7 @@ declare module DebugProtocol { } /** Response to 'disassemble' request. */ - export interface DisassembleResponse extends Response { + interface DisassembleResponse extends Response { body?: { /** The list of disassembled instructions. */ instructions: DisassembledInstruction[]; @@ -1643,7 +1644,7 @@ declare module DebugProtocol { } /** Information about the capabilities of a debug adapter. */ - export interface Capabilities { + interface Capabilities { /** The debug adapter supports the 'configurationDone' request. */ supportsConfigurationDoneRequest?: boolean; /** The debug adapter supports function breakpoints. */ @@ -1725,7 +1726,7 @@ declare module DebugProtocol { } /** An ExceptionBreakpointsFilter is shown in the UI as an filter option for configuring how exceptions are dealt with. */ - export interface ExceptionBreakpointsFilter { + interface ExceptionBreakpointsFilter { /** The internal ID of the filter option. This value is passed to the 'setExceptionBreakpoints' request. */ filter: string; /** The name of the filter option. This will be shown in the UI. */ @@ -1741,7 +1742,7 @@ declare module DebugProtocol { } /** A structured message object. Used to return errors from requests. */ - export interface Message { + interface Message { /** Unique identifier for the message. */ id: number; /** A format string for the message. Embedded variables have the form '{name}'. @@ -1769,7 +1770,7 @@ declare module DebugProtocol { To avoid an unnecessary proliferation of additional attributes with similar semantics but different names we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. */ - export interface Module { + interface Module { /** Unique identifier for the module. */ id: number | string; /** A name of the module. */ @@ -1800,7 +1801,7 @@ declare module DebugProtocol { and what the column's label should be. It is only used if the underlying UI actually supports this level of customization. */ - export interface ColumnDescriptor { + interface ColumnDescriptor { /** Name of the attribute rendered in this column. */ attributeName: string; /** Header UI label of column. */ @@ -1816,12 +1817,12 @@ declare module DebugProtocol { /** The ModulesViewDescriptor is the container for all declarative configuration options of a ModuleView. For now it only specifies the columns to be shown in the modules view. */ - export interface ModulesViewDescriptor { + interface ModulesViewDescriptor { columns: ColumnDescriptor[]; } /** A Thread */ - export interface Thread { + interface Thread { /** Unique identifier for the thread. */ id: number; /** A name of the thread. */ @@ -1831,7 +1832,7 @@ declare module DebugProtocol { /** A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. */ - export interface Source { + interface Source { /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ @@ -1862,7 +1863,7 @@ declare module DebugProtocol { } /** A Stackframe contains the source location. */ - export interface StackFrame { + interface StackFrame { /** An identifier for the stack frame. It must be unique across all threads. This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. */ @@ -1892,7 +1893,7 @@ declare module DebugProtocol { } /** A Scope is a named container for variables. Optionally a scope can map to a source or a range within a source. */ - export interface Scope { + interface Scope { /** Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This string is shown in the UI as is and can be translated. */ name: string; /** An optional hint for how to present this scope in the UI. If this attribute is missing, the scope is shown with a generic UI. @@ -1934,7 +1935,7 @@ declare module DebugProtocol { If the number of named or indexed children is large, the numbers should be returned via the optional 'namedVariables' and 'indexedVariables' attributes. The client can use this optional information to present the children in a paged UI and fetch them in chunks. */ - export interface Variable { + interface Variable { /** The variable's name. */ name: string; /** The variable's value. @@ -1968,7 +1969,7 @@ declare module DebugProtocol { } /** Optional properties of a variable that can be used to determine how to render the variable in the UI. */ - export interface VariablePresentationHint { + interface VariablePresentationHint { /** The kind of variable. Before introducing additional values, try to use the listed values. Values: 'property': Indicates that the object is a property. @@ -2011,7 +2012,7 @@ declare module DebugProtocol { } /** Properties of a breakpoint location returned from the 'breakpointLocations' request. */ - export interface BreakpointLocation { + interface BreakpointLocation { /** Start line of breakpoint location. */ line: number; /** Optional start column of breakpoint location. */ @@ -2023,7 +2024,7 @@ declare module DebugProtocol { } /** Properties of a breakpoint or logpoint passed to the setBreakpoints request. */ - export interface SourceBreakpoint { + interface SourceBreakpoint { /** The source line of the breakpoint or logpoint. */ line: number; /** An optional source column of the breakpoint. */ @@ -2045,7 +2046,7 @@ declare module DebugProtocol { } /** Properties of a breakpoint passed to the setFunctionBreakpoints request. */ - export interface FunctionBreakpoint { + interface FunctionBreakpoint { /** The name of the function. */ name: string; /** An optional expression for conditional breakpoints. @@ -2060,10 +2061,10 @@ declare module DebugProtocol { } /** This enumeration defines all possible access types for data breakpoints. */ - export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; + type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; /** Properties of a data breakpoint passed to the setDataBreakpoints request. */ - export interface DataBreakpoint { + interface DataBreakpoint { /** An id representing the data. This id is returned from the dataBreakpointInfo request. */ dataId: string; /** The access type of the data. */ @@ -2077,7 +2078,7 @@ declare module DebugProtocol { } /** Properties of a breakpoint passed to the setInstructionBreakpoints request */ - export interface InstructionBreakpoint { + interface InstructionBreakpoint { /** The instruction reference of the breakpoint. This should be a memory or instruction pointer reference from an EvaluateResponse, Variable, StackFrame, GotoTarget, or Breakpoint. */ @@ -2098,7 +2099,7 @@ declare module DebugProtocol { } /** Information about a Breakpoint created in setBreakpoints, setFunctionBreakpoints, setInstructionBreakpoints, or setDataBreakpoints. */ - export interface Breakpoint { + interface Breakpoint { /** An optional identifier for the breakpoint. It is needed if breakpoint events are used to update or remove breakpoints. */ id?: number; /** If true breakpoint could be set (but not necessarily at the desired location). */ @@ -2134,10 +2135,10 @@ declare module DebugProtocol { 'line': The step should allow the program to run until the current source line has executed. 'instruction': The step should allow one instruction to execute (e.g. one x86 instruction). */ - export type SteppingGranularity = 'statement' | 'line' | 'instruction'; + type SteppingGranularity = 'statement' | 'line' | 'instruction'; /** A StepInTarget can be used in the 'stepIn' request and determines into which single target the stepIn request should step. */ - export interface StepInTarget { + interface StepInTarget { /** Unique identifier for a stepIn target. */ id: number; /** The name of the stepIn target (shown in the UI). */ @@ -2147,7 +2148,7 @@ declare module DebugProtocol { /** A GotoTarget describes a code location that can be used as a target in the 'goto' request. The possible goto targets can be determined via the 'gotoTargets' request. */ - export interface GotoTarget { + interface GotoTarget { /** Unique identifier for a goto target. This is used in the goto request. */ id: number; /** The name of the goto target (shown in the UI). */ @@ -2165,7 +2166,7 @@ declare module DebugProtocol { } /** CompletionItems are the suggestions returned from the CompletionsRequest. */ - export interface CompletionItem { + interface CompletionItem { /** The label of this completion item. By default this is also the text that is inserted when selecting this completion. */ label: string; /** If text is not falsy then it is inserted instead of the label. */ @@ -2197,13 +2198,13 @@ declare module DebugProtocol { } /** Some predefined types for the CompletionItem. Please note that not all clients have specific icons for all of them. */ - export type CompletionItemType = 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file' | 'reference' | 'customcolor'; + type CompletionItemType = 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file' | 'reference' | 'customcolor'; /** Names of checksum algorithms that may be supported by a debug adapter. */ - export type ChecksumAlgorithm = 'MD5' | 'SHA1' | 'SHA256' | 'timestamp'; + type ChecksumAlgorithm = 'MD5' | 'SHA1' | 'SHA256' | 'timestamp'; /** The checksum of an item calculated by the specified algorithm. */ - export interface Checksum { + interface Checksum { /** The algorithm used to calculate this checksum. */ algorithm: ChecksumAlgorithm; /** Value of the checksum. */ @@ -2211,13 +2212,13 @@ declare module DebugProtocol { } /** Provides formatting information for a value. */ - export interface ValueFormat { + interface ValueFormat { /** Display the value in hex. */ hex?: boolean; } /** Provides formatting information for a stack frame. */ - export interface StackFrameFormat extends ValueFormat { + interface StackFrameFormat extends ValueFormat { /** Displays parameters for the stack frame. */ parameters?: boolean; /** Displays the types of parameters for the stack frame. */ @@ -2234,8 +2235,8 @@ declare module DebugProtocol { includeAll?: boolean; } - /** An ExceptionFilterOptions is used to specify an exception filter together with a condition for the setExceptionsFilter request. */ - export interface ExceptionFilterOptions { + /** An ExceptionFilterOptions is used to specify an exception filter together with a condition for the 'setExceptionBreakpoints' request. */ + interface ExceptionFilterOptions { /** ID of an exception filter returned by the 'exceptionBreakpointFilters' capability. */ filterId: string; /** An optional expression for conditional exceptions. @@ -2245,7 +2246,7 @@ declare module DebugProtocol { } /** An ExceptionOptions assigns configuration options to a set of exceptions. */ - export interface ExceptionOptions { + interface ExceptionOptions { /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ @@ -2260,13 +2261,13 @@ declare module DebugProtocol { unhandled: breaks when exception unhandled, userUnhandled: breaks if the exception is not handled by user code. */ - export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; + type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or it matches anything except the names provided if 'negate' is true. */ - export interface ExceptionPathSegment { + interface ExceptionPathSegment { /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ negate?: boolean; /** Depending on the value of 'negate' the names that should match or not match. */ @@ -2274,7 +2275,7 @@ declare module DebugProtocol { } /** Detailed information about an exception that has occurred. */ - export interface ExceptionDetails { + interface ExceptionDetails { /** Message contained in the exception. */ message?: string; /** Short type name of the exception object. */ @@ -2290,7 +2291,7 @@ declare module DebugProtocol { } /** Represents a single disassembled instruction. */ - export interface DisassembledInstruction { + interface DisassembledInstruction { /** The address of the instruction. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ address: string; /** Optional raw bytes representing the instruction and its operands, in an implementation-defined format. */ @@ -2322,6 +2323,6 @@ declare module DebugProtocol { 'variables': Previously fetched variable data has become invalid and needs to be refetched. etc. */ - export type InvalidatedAreas = 'all' | 'stacks' | 'threads' | 'variables' | string; + type InvalidatedAreas = 'all' | 'stacks' | 'threads' | 'variables' | string; } diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 8f1278c1cd6..72ff6b21db2 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -12,7 +12,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { URI } from 'vs/base/common/uri'; import { ExecutableDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { TestTextResourcePropertiesService } from 'vs/editor/test/common/services/testTextResourcePropertiesService'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from 'vs/platform/extensions/common/extensions'; suite('Debug - Debugger', () => { @@ -58,6 +58,7 @@ suite('Debug - Debugger', () => { isUserBuiltin: false, isUnderDevelopment: false, engines: null!, + targetPlatform: TargetPlatform.UNDEFINED, contributes: { 'debuggers': [ debuggerContribution @@ -76,6 +77,7 @@ suite('Debug - Debugger', () => { isUserBuiltin: false, isUnderDevelopment: false, engines: null!, + targetPlatform: TargetPlatform.UNDEFINED, contributes: { 'debuggers': [ { @@ -100,6 +102,7 @@ suite('Debug - Debugger', () => { isUserBuiltin: false, isUnderDevelopment: false, engines: null!, + targetPlatform: TargetPlatform.UNDEFINED, contributes: { 'debuggers': [ { diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 14b181d1828..ed27525eb03 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -11,7 +11,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IExtensionService, IExtensionsStatus, IExtensionHostProfile, ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IExtensionsStatus, IExtensionHostProfile, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { append, $, Dimension, clearNode, addDisposableListener } from 'vs/base/browser/dom'; @@ -23,7 +23,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { memoize } from 'vs/base/common/decorators'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; @@ -34,8 +34,9 @@ import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RuntimeExtensionsInput } from 'vs/workbench/contrib/extensions/common/runtimeExtensionsInput'; -import { Action2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; interface IExtensionProfileInformation { /** @@ -80,6 +81,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IStorageService storageService: IStorageService, @ILabelService private readonly _labelService: ILabelService, @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IClipboardService private readonly _clipboardService: IClipboardService, ) { super(AbstractRuntimeExtensionsEditor.ID, telemetryService, themeService, storageService); @@ -370,15 +372,17 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } let extraLabel: string | null = null; - if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { + if (element.status.runningLocation && element.status.runningLocation.equals(new LocalWebWorkerRunningLocation())) { + extraLabel = `$(globe) web worker`; + } else if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { const hostLabel = this._labelService.getHostLabel(Schemas.vscodeRemote, this._environmentService.remoteAuthority); if (hostLabel) { extraLabel = `$(remote) ${hostLabel}`; } else { extraLabel = `$(remote) ${element.description.extensionLocation.authority}`; } - } else if (element.status.runningLocation === ExtensionRunningLocation.LocalWebWorker) { - extraLabel = `$(globe) web worker`; + } else if (element.status.runningLocation && element.status.runningLocation.affinity > 0) { + extraLabel = `$(server-process) local process ${element.status.runningLocation.affinity + 1}`; } if (extraLabel) { @@ -427,11 +431,21 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { const actions: IAction[] = []; + actions.push(new Action( + 'runtimeExtensionsEditor.action.copyId', + nls.localize('copy id', "Copy id ({0})", e.element!.description.identifier.value), + undefined, + true, + () => { + this._clipboardService.writeText(e.element!.description.identifier.value); + } + )); + const reportExtensionIssueAction = this._createReportExtensionIssueAction(e.element); if (reportExtensionIssueAction) { actions.push(reportExtensionIssueAction); - actions.push(new Separator()); } + actions.push(new Separator()); if (e.element!.marketplaceInfo) { actions.push(new Action('runtimeExtensionsEditor.action.disableWorkspace', nls.localize('disable workspace', "Disable (Workspace)"), undefined, true, () => this._extensionsWorkbenchService.setEnablement(e.element!.marketplaceInfo!, EnablementState.DisabledWorkspace))); @@ -481,7 +495,13 @@ export class ShowRuntimeExtensionsAction extends Action2 { id: 'workbench.action.showRuntimeExtensions', title: { value: nls.localize('showRuntimeExtensions', "Show Running Extensions"), original: 'Show Running Extensions' }, category: CATEGORIES.Developer, - f1: true + f1: true, + menu: { + id: MenuId.ViewContainerTitle, + when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'), + group: '2_enablement', + order: 3 + } }); } diff --git a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts index a3b1e809050..87a4c50df78 100644 --- a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts @@ -4,38 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from 'vs/base/common/actions'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IExtensionService, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ILabelService } from 'vs/platform/label/common/label'; +import { IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from 'vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor'; export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService, - @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionService extensionService: IExtensionService, - @INotificationService notificationService: INotificationService, - @IContextMenuService contextMenuService: IContextMenuService, - @IInstantiationService instantiationService: IInstantiationService, - @IStorageService storageService: IStorageService, - @ILabelService labelService: ILabelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService); - } - protected _getProfileInfo(): IExtensionHostProfile | null { return null; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index f01d6cc1a4b..d943afa481d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -824,7 +824,7 @@ export class ExtensionEditor extends EditorPane { height: 40px; right: 25px; bottom: 25px; - background-color:#444444; + background-color:var(--vscode-button-background); border-radius: 50%; cursor: pointer; box-shadow: 1px 1px 1px rgba(0,0,0,.25); @@ -835,7 +835,7 @@ export class ExtensionEditor extends EditorPane { } #scroll-to-top:hover { - background-color:#007acc; + background-color:var(--vscode-button-hoverBackground); box-shadow: 2px 2px 2px rgba(0,0,0,.25); } @@ -999,11 +999,11 @@ export class ExtensionEditor extends EditorPane { append(moreInfo, $('.more-info-entry', undefined, $('div', undefined, localize('release date', "Released on")), - $('div', undefined, new Date(gallery.releaseDate).toLocaleString(undefined, { hour12: false })) + $('div', undefined, new Date(gallery.releaseDate).toLocaleString(undefined, { hourCycle: 'h23' })) ), $('.more-info-entry', undefined, $('div', undefined, localize('last updated', "Last updated")), - $('div', undefined, new Date(gallery.lastUpdated).toLocaleString(undefined, { hour12: false })) + $('div', undefined, new Date(gallery.lastUpdated).toLocaleString(undefined, { hourCycle: 'h23' })) ) ); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 5183cae9573..879e140dc96 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,7 +8,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { MenuRegistry, MenuId, registerAction2, Action2, ISubmenuItem, IMenuItem, IAction2Options } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, InstallOptions, getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -25,7 +25,7 @@ import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from ' import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, getIdAndVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { EditorPaneDescriptor, IEditorPaneRegistry } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; @@ -170,6 +170,9 @@ Registry.as(ConfigurationExtensions.Configuration) }, 'extensions.confirmedUriHandlerExtensionIds': { type: 'array', + items: { + type: 'string' + }, description: localize('handleUriConfirmedExtensions', "When an extension is listed here, a confirmation prompt will not be shown when that extension handles a URI."), default: [], scope: ConfigurationScope.APPLICATION @@ -194,6 +197,7 @@ Registry.as(ConfigurationExtensions.Configuration) default: false } }, + additionalProperties: false, default: { 'pub.name': false } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8f6e11bee28..2de5c8e1411 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -745,14 +745,7 @@ export class UpdateAction extends ExtensionAction { if (!this.extension) { this.enabled = false; this.class = UpdateAction.DisabledClass; - this.label = this.getUpdateLabel(); - return; - } - - if (this.extension.type !== ExtensionType.User) { - this.enabled = false; - this.class = UpdateAction.DisabledClass; - this.label = this.getUpdateLabel(); + this.label = this.getLabel(); return; } @@ -761,7 +754,7 @@ export class UpdateAction extends ExtensionAction { this.enabled = canInstall && isInstalled && this.extension.outdated; this.class = this.enabled ? UpdateAction.EnabledClass : UpdateAction.DisabledClass; - this.label = this.extension.outdated ? this.getUpdateLabel(this.extension.latestVersion) : this.getUpdateLabel(); + this.label = this.getLabel(this.extension); } override async run(): Promise { @@ -781,8 +774,14 @@ export class UpdateAction extends ExtensionAction { } } - private getUpdateLabel(version?: string): string { - return version ? localize('updateTo', "Update to {0}", version) : localize('updateAction', "Update"); + private getLabel(extension?: IExtension): string { + if (!extension?.outdated) { + return localize('updateAction', "Update"); + } + if (extension.outdatedTargetPlatform) { + return localize('updateToTargetPlatformVersion', "Update to {0} version", TargetPlatformToString(extension.gallery!.properties.targetPlatform)); + } + return localize('updateToLatestVersion', "Update to {0}", extension.latestVersion); } } @@ -1421,8 +1420,8 @@ export class ReloadAction extends ExtensionAction { const runningExtensionServer = this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); if (isSameExtensionRunning) { - // Different version of same extension is running. Requires reload to run the current version - if (this.extension.version !== runningExtension.version) { + // Different version or target platform of same extension is running. Requires reload to run the current version + if (this.extension.version !== runningExtension.version || this.extension.local.targetPlatform !== runningExtension.targetPlatform) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postUpdateTooltip', "Please reload Visual Studio Code to enable the updated extension."); @@ -2727,19 +2726,22 @@ CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsWith export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { dark: buttonBackground, light: buttonBackground, - hc: null + hcDark: null, + hcLight: null }, localize('extensionButtonProminentBackground', "Button background color for actions extension that stand out (e.g. install button).")); export const extensionButtonProminentForeground = registerColor('extensionButton.prominentForeground', { dark: buttonForeground, light: buttonForeground, - hc: null + hcDark: null, + hcLight: null }, localize('extensionButtonProminentForeground', "Button foreground color for actions extension that stand out (e.g. install button).")); export const extensionButtonProminentHoverBackground = registerColor('extensionButton.prominentHoverBackground', { dark: buttonHoverBackground, light: buttonHoverBackground, - hc: null + hcDark: null, + hcLight: null }, localize('extensionButtonProminentHoverBackground', "Button background hover color for actions extension that stand out (e.g. install button).")); registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 88443ea1928..f51545869aa 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -12,7 +12,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { Action } from 'vs/base/common/actions'; -import { append, $, Dimension, hide, show } from 'vs/base/browser/dom'; +import { append, $, Dimension, hide, show, DragAndDropObserver } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -49,7 +49,6 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; -import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 751a91aab0a..522cc6346be 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -592,9 +592,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { } // Rating icon -export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hc: '#FF8E00' }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true); -export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hc: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); -export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hc: '#1d9271' }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true); +export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), true); +export const extensionVerifiedPublisherIconColor = registerColor('extensionIcon.verifiedForeground', { dark: textLinkForeground, light: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('extensionIconVerifiedForeground', "The icon color for extension verified publisher."), true); +export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), true); registerThemingParticipant((theme, collector) => { const extensionRatingIcon = theme.getColor(extensionRatingIconColor); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 4919f111cc4..c63dc413e9f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -18,7 +18,7 @@ import { IExtensionsControlManifest, InstallVSIXOptions, IExtensionInfo, IExtensionQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, DefaultIconPath } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionIdentifierWithVersion, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, groupByExtension, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -34,7 +34,7 @@ import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IProductService } from 'vs/platform/product/common/productService'; import { FileAccess } from 'vs/base/common/network'; @@ -45,12 +45,13 @@ import { isBoolean, isUndefined } from 'vs/base/common/types'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IExtensionService, IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; +import { isWeb } from 'vs/base/common/platform'; interface IExtensionStateProvider { (extension: Extension): T; } -class Extension implements IExtension { +export class Extension implements IExtension { public enablementState: EnablementState = EnablementState.EnabledGlobally; @@ -202,7 +203,27 @@ class Extension implements IExtension { } get outdated(): boolean { - return !!this.gallery && this.type === ExtensionType.User && semver.gt(this.latestVersion, this.version) && (this.local?.preRelease || !this.gallery?.properties.isPreReleaseVersion); + if (!this.gallery || !this.local) { + return false; + } + if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { + return false; + } + if (semver.gt(this.latestVersion, this.version)) { + return true; + } + if (this.outdatedTargetPlatform) { + return true; + } + return false; + } + + get outdatedTargetPlatform(): boolean { + return !!this.local && !!this.gallery + && ![TargetPlatform.UNDEFINED, TargetPlatform.WEB].includes(this.local.targetPlatform) + && this.gallery.properties.targetPlatform !== TargetPlatform.WEB + && this.local.targetPlatform !== this.gallery.properties.targetPlatform + && semver.eq(this.latestVersion, this.version); } get telemetryData(): any { @@ -436,7 +457,7 @@ class Extensions extends Disposable { if (extension.local && !extension.local.identifier.uuid) { extension.local = await this.updateMetadata(extension.local, gallery); } - if (!extension.gallery || extension.gallery.version !== gallery.version) { + if (!extension.gallery || extension.gallery.version !== gallery.version || extension.gallery.properties.targetPlatform !== gallery.properties.targetPlatform) { extension.gallery = gallery; this._onChange.fire({ extension }); hasChanged = true; @@ -695,8 +716,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.queryLocal().then(() => { this.resetIgnoreAutoUpdateExtensions(); this.eventuallyCheckForUpdates(true); - // Always auto update builtin extensions - if (!this.isAutoUpdateEnabled()) { + // Always auto update builtin extensions in web + if (isWeb && !this.isAutoUpdateEnabled()) { this.autoUpdateBuiltinExtensions(); } }); @@ -1032,7 +1053,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const infos: IExtensionInfo[] = []; for (const installed of this.local) { - if (installed.type === ExtensionType.User && (!onlyBuiltin || installed.isBuiltin)) { + if (!onlyBuiltin || installed.isBuiltin) { infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); } } @@ -1104,7 +1125,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const toUpdate = this.outdated.filter(e => - !this.isAutoUpdateIgnored(new ExtensionIdentifierWithVersion(e.identifier, e.version)) && + !this.isAutoUpdateIgnored(new ExtensionKey(e.identifier, e.version)) && (this.getAutoUpdateValue() === true || (e.local && this.extensionEnablementService.isEnabled(e.local))) ); @@ -1198,7 +1219,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension installOptions.installGivenVersion = true; const installed = await this.installFromGallery(extension, gallery, installOptions); if (extension.latestVersion !== version) { - this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(gallery.identifier, version)); + this.ignoreAutoUpdate(new ExtensionKey(gallery.identifier, version)); } return installed; }, gallery.displayName); @@ -1269,7 +1290,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension const { identifier } = await this.extensionManagementService.install(vsix, installOptions); if (existingExtension && existingExtension.latestVersion !== manifest.version) { - this.ignoreAutoUpdate(new ExtensionIdentifierWithVersion(identifier, manifest.version)); + this.ignoreAutoUpdate(new ExtensionKey(identifier, manifest.version)); } return this.local.filter(local => areSameExtensions(local.identifier, identifier))[0]; @@ -1510,18 +1531,18 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.storageService.store('extensions.ignoredAutoUpdateExtension', JSON.stringify(this._ignoredAutoUpdateExtensions), StorageScope.GLOBAL, StorageTarget.MACHINE); } - private ignoreAutoUpdate(identifierWithVersion: ExtensionIdentifierWithVersion): void { - if (!this.isAutoUpdateIgnored(identifierWithVersion)) { - this.ignoredAutoUpdateExtensions = [...this.ignoredAutoUpdateExtensions, identifierWithVersion.key()]; + private ignoreAutoUpdate(extensionKey: ExtensionKey): void { + if (!this.isAutoUpdateIgnored(extensionKey)) { + this.ignoredAutoUpdateExtensions = [...this.ignoredAutoUpdateExtensions, extensionKey.toString()]; } } - private isAutoUpdateIgnored(identifierWithVersion: ExtensionIdentifierWithVersion): boolean { - return this.ignoredAutoUpdateExtensions.indexOf(identifierWithVersion.key()) !== -1; + private isAutoUpdateIgnored(extensionKey: ExtensionKey): boolean { + return this.ignoredAutoUpdateExtensions.indexOf(extensionKey.toString()) !== -1; } private resetIgnoreAutoUpdateExtensions(): void { - this.ignoredAutoUpdateExtensions = this.ignoredAutoUpdateExtensions.filter(extensionId => this.local.some(local => !!local.local && new ExtensionIdentifierWithVersion(local.identifier, local.version).key() === extensionId)); + this.ignoredAutoUpdateExtensions = this.ignoredAutoUpdateExtensions.filter(extensionId => this.local.some(local => !!local.local && new ExtensionKey(local.identifier, local.version).toString() === extensionId)); } } diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index 51398377e56..b81342b1bb9 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -6,7 +6,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -104,8 +104,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations { @IStorageService private readonly storageService: IStorageService, @IExtensionRecommendationNotificationService private readonly extensionRecommendationNotificationService: IExtensionRecommendationNotificationService, @IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService, - @IWorkbenchAssignmentService private tasExperimentService: IWorkbenchAssignmentService, - @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IWorkbenchAssignmentService private readonly tasExperimentService: IWorkbenchAssignmentService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, ) { super(); @@ -245,17 +246,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - fileExtension = fileExtension.substr(1); // Strip the dot - if (!fileExtension) { - return; - } - - const mimeTypes = getMimeTypes(uri); - if (mimeTypes.length !== 1 || mimeTypes[0] !== Mimes.unknown) { - return; - } - - this.promptRecommendedExtensionForFileExtension(fileExtension, installed); + this.promptRecommendedExtensionForFileExtension(uri, fileExtension, installed); } private async promptRecommendedExtensionForFileType(name: string, language: string, recommendations: string[], installed: IExtension[]): Promise { @@ -313,7 +304,22 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.storageService.store(promptedFileExtensionsStorageKey, JSON.stringify(distinct(promptedFileExtensions)), StorageScope.GLOBAL, StorageTarget.USER); } - private async promptRecommendedExtensionForFileExtension(fileExtension: string, installed: IExtension[]): Promise { + private async promptRecommendedExtensionForFileExtension(uri: URI, fileExtension: string, installed: IExtension[]): Promise { + // Do not prompt when there is no local and remote extension management servers + if (!this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer) { + return; + } + + fileExtension = fileExtension.substring(1); // Strip the dot + if (!fileExtension) { + return; + } + + const mimeTypes = getMimeTypes(uri); + if (mimeTypes.length !== 1 || mimeTypes[0] !== Mimes.unknown) { + return; + } + const fileExtensionSuggestionIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/fileExtensionsSuggestionIgnore', StorageScope.GLOBAL, '[]')); if (fileExtensionSuggestionIgnoreList.indexOf(fileExtension) > -1) { return; diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 045a90e2073..f9b66889af2 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -213,7 +213,8 @@ padding: 1px 2px; } -.hc-black .extension-list-item .monaco-action-bar .action-label.icon { +.hc-black .extension-list-item .monaco-action-bar .action-label.icon, +.hc-light .extension-list-item .monaco-action-bar .action-label.icon { padding: 0px 2px; } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index b6744153baf..b3846d038fc 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -61,6 +61,7 @@ export interface IExtension { readonly rating?: number; readonly ratingCount?: number; readonly outdated: boolean; + readonly outdatedTargetPlatform: boolean; readonly enablementState: EnablementState; readonly tags: readonly string[]; readonly categories: readonly string[]; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 6787e95d718..b411342d32e 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { Action } from 'vs/base/common/actions'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -29,8 +29,8 @@ export class DebugExtensionHostAction extends Action { override async run(): Promise { - const inspectPort = await this._extensionService.getInspectPort(false); - if (!inspectPort) { + const inspectPorts = await this._extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false); + if (inspectPorts.length === 0) { const res = await this._dialogService.confirm({ type: 'info', message: nls.localize('restart1', "Profile Extensions"), @@ -45,11 +45,16 @@ export class DebugExtensionHostAction extends Action { return; } + if (inspectPorts.length > 1) { + // TODO + console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); + } + return this._debugService.startDebugging(undefined, { type: 'node', name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"), request: 'attach', - port: inspectPort + port: inspectPorts[0] }); } } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts index c6efc4ff567..44295a6868b 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtensionHostProfile, ProfileSession, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHostProfile, ProfileSession, IExtensionService, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { onUnexpectedError } from 'vs/base/common/errors'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/browser/statusbar'; @@ -116,8 +116,9 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio return null; } - const inspectPort = await this._extensionService.getInspectPort(true); - if (!inspectPort) { + const inspectPorts = await this._extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, true); + + if (inspectPorts.length === 0) { return this._dialogService.confirm({ type: 'info', message: nls.localize('restart1', "Profile Extensions"), @@ -131,9 +132,14 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio }); } + if (inspectPorts.length > 1) { + // TODO + console.warn(`There are multiple extension hosts available for profiling. Picking the first one...`); + } + this._setState(ProfileSessionState.Starting); - return this._instantiationService.createInstance(ExtensionHostProfiler, inspectPort).start().then((value) => { + return this._instantiationService.createInstance(ExtensionHostProfiler, inspectPorts[0]).start().then((value) => { this._profileSession = value; this._setState(ProfileSessionState.Running); }, (err) => { diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts index e967b931728..ba9f426ab94 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionsAutoProfiler.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IExtensionService, IResponsiveStateChangeEvent, IExtensionHostProfile, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IResponsiveStateChangeEvent, IExtensionHostProfile, ProfileSession, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; @@ -46,8 +46,11 @@ export class ExtensionsAutoProfiler extends Disposable implements IWorkbenchCont } private async _onDidChangeResponsiveChange(event: IResponsiveStateChangeEvent): Promise { + if (event.extensionHostKind !== ExtensionHostKind.LocalProcess) { + return; + } - const port = await this._extensionService.getInspectPort(true); + const port = await this._extensionService.getInspectPort(event.extensionHostId, true); if (!port) { return; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index ca31c6145d1..b53ea1625f5 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -26,6 +26,7 @@ import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IV8Profile, Utils } from 'vs/platform/profiling/common/profiling'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -72,9 +73,10 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IStorageService storageService: IStorageService, @ILabelService labelService: ILabelService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IClipboardService clipboardService: IClipboardService, @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, ) { - super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService); + super(telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts new file mode 100644 index 00000000000..335679af2a9 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; +import { Extension } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IGalleryExtension, IGalleryExtensionProperties, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { URI } from 'vs/base/common/uri'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { generateUuid } from 'vs/base/common/uuid'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; + +suite('Extension Test', () => { + + let instantiationService: TestInstantiationService; + + setup(() => { + instantiationService = new TestInstantiationService(); + }); + + test('extension is not outdated when there is no local and gallery', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, undefined, undefined); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is not outdated when there is local and no gallery', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension(), undefined); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is not outdated when there is no local and has gallery', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, undefined, aGalleryExtension()); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is not outdated when local and gallery are on same version', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension(), aGalleryExtension()); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is outdated when local is older than gallery', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is outdated when local is built in and older than gallery', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is outdated when local and gallery are on same version but on different target platforms', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is not outdated when local and gallery are on same version and local is on web', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WEB }), aGalleryExtension('somext')); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is not outdated when local and gallery are on same version and gallery is on web', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext'), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WEB })); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is not outdated when local is not pre-release but gallery is pre-release', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + assert.strictEqual(extension.outdated, false); + }); + + test('extension is outdated when local and gallery are pre-releases', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is outdated when local was opted to pre-release but current version is not pre-release', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' }, { isPreReleaseVersion: true })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is outdated when local is pre-release but gallery is not', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: true }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, true); + }); + + test('extension is outdated when local was opted pre-release but current version is not and gallery is not', () => { + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { preRelease: true, isPreReleaseVersion: false }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, true); + }); + + function aLocalExtension(name: string = 'someext', manifest: Partial = {}, properties: Partial = {}): ILocalExtension { + manifest = { name, publisher: 'pub', version: '1.0.0', ...manifest }; + properties = { + type: ExtensionType.User, + location: URI.file(`pub.${name}`), + identifier: { id: getGalleryExtensionId(manifest.publisher!, manifest.name!) }, + targetPlatform: TargetPlatform.UNDEFINED, + ...properties + }; + return Object.create({ manifest, ...properties }); + } + + function aGalleryExtension(name: string = 'somext', properties: Partial = {}, galleryExtensionProperties: Partial = {}): IGalleryExtension { + const targetPlatform = galleryExtensionProperties.targetPlatform ?? TargetPlatform.UNDEFINED; + const galleryExtension = Object.create({ name, publisher: 'pub', version: '1.0.0', allTargetPlatforms: [targetPlatform], properties: {}, assets: {}, ...properties }); + galleryExtension.properties = { ...galleryExtension.properties, dependencies: [], targetPlatform, ...galleryExtensionProperties }; + galleryExtension.identifier = { id: getGalleryExtensionId(galleryExtension.publisher, galleryExtension.name), uuid: generateUuid() }; + return galleryExtension; + } + +}); diff --git a/src/vs/workbench/contrib/feedback/browser/media/feedback.css b/src/vs/workbench/contrib/feedback/browser/media/feedback.css index 409ebc60eb2..e619571200f 100644 --- a/src/vs/workbench/contrib/feedback/browser/media/feedback.css +++ b/src/vs/workbench/contrib/feedback/browser/media/feedback.css @@ -200,12 +200,21 @@ outline-offset: -2px; } +.monaco-workbench.hc-light .feedback-form { + outline: 2px solid #0F4A85; + outline-offset: -2px; +} + + .monaco-workbench.hc-black .feedback-form .feedback-alias, -.monaco-workbench.hc-black .feedback-form .feedback-description { +.monaco-workbench.hc-black .feedback-form .feedback-description, +.monaco-workbench.hc-light .feedback-form .feedback-alias, +.monaco-workbench.hc-light .feedback-form .feedback-description { font-family: inherit; } -.monaco-workbench.hc-black .feedback-form .content .contactus { +.monaco-workbench.hc-black .feedback-form .content .contactus, +.monaco-workbench.hc-light .feedback-form .content .contactus { padding: 10px; float: right; } @@ -218,6 +227,14 @@ border: 1px solid #6FC3DF; } +.monaco-workbench.hc-light .feedback-form .form-buttons .send, +.monaco-workbench.hc-light .feedback-form .form-buttons .send.in-progress, +.monaco-workbench.hc-light .feedback-form .form-buttons .send.success { + background-color: #FFFFFF; + color: #292929; + border: 1px solid #0F4A85; +} + .monaco-workbench.hc-black .feedback-form .form-buttons .send:hover { background-color: #0C141F; } diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index a6419b54864..630cd3a523a 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -176,7 +176,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements private allowLabelOverride(): boolean { return this.resource.scheme !== this.pathService.defaultUriScheme && - this.resource.scheme !== Schemas.userData && + this.resource.scheme !== Schemas.vscodeUserData && this.resource.scheme !== Schemas.file && this.resource.scheme !== Schemas.vscodeRemote; } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 4d217221755..b323eb64873 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -33,6 +33,7 @@ import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** * An implementation of editor for file system resources. @@ -57,7 +58,8 @@ export class TextFileEditor extends BaseTextEditor { @ITextFileService private readonly textFileService: ITextFileService, @IExplorerService private readonly explorerService: IExplorerService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); @@ -210,18 +212,29 @@ export class TextFileEditor extends BaseTextEditor { private openAsBinary(input: FileEditorInput, options: ITextEditorOptions | undefined): void { - // Mark file input for forced binary opening - input.setForceOpenAsBinary(); - - // Open in group - (this.group ?? this.editorGroupService.activeGroup).openEditor(input, { + const defaultBinaryEditor = this.configurationService.getValue('workbench.editor.defaultBinaryEditor'); + const groupToOpen = this.group ?? this.editorGroupService.activeGroup; + const editorOptions = { ...options, // Make sure to not steal away the currently active group // because we are triggering another openEditor() call // and do not control the initial intent that resulted // in us now opening as binary. activation: EditorActivation.PRESERVE - }); + }; + + // If we the user setting specifies a default binary editor we use that. + if (defaultBinaryEditor && defaultBinaryEditor !== '') { + this.editorService.replaceEditors([{ + editor: input, + replacement: { resource: input.resource, options: { ...editorOptions, override: defaultBinaryEditor } } + }], groupToOpen); + } else { + // Mark file input for forced binary opening + input.setForceOpenAsBinary(); + // Open in group + groupToOpen.openEditor(input, editorOptions); + } } private async openAsFolder(input: FileEditorInput): Promise { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 1f60c6af30c..95c4806a86f 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -19,7 +19,7 @@ import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/browser/editors/fileEditorInput'; -import { SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileConstants'; import { INotificationService, INotificationHandle, INotificationActions, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -67,7 +67,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } private registerListeners(): void { - this._register(this.textFileService.files.onDidSave(event => this.onFileSavedOrReverted(event.model.resource))); + this._register(this.textFileService.files.onDidSave(e => this.onFileSavedOrReverted(e.model.resource))); this._register(this.textFileService.files.onDidRevert(model => this.onFileSavedOrReverted(model.resource))); this._register(this.editorService.onDidActiveEditorChange(() => this.onActiveEditorChanged())); } diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index a12970017cf..9965ab09ae9 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -128,7 +128,12 @@ export class ExplorerService implements IExplorerService { } })); // Refresh explorer when window gets focus to compensate for missing file events #126817 - this.disposables.add(hostService.onDidChangeFocus(hasFocus => hasFocus ? this.refresh(false) : undefined)); + const skipRefreshExplorerOnWindowFocus = this.configurationService.getValue('skipRefreshExplorerOnWindowFocus'); + this.disposables.add(hostService.onDidChangeFocus(hasFocus => { + if (!skipRefreshExplorerOnWindowFocus && hasFocus) { + this.refresh(false); + } + })); } get roots(): ExplorerItem[] { @@ -146,11 +151,20 @@ export class ExplorerService implements IExplorerService { this.view = contextProvider; } - getContext(respectMultiSelection: boolean): ExplorerItem[] { + getContext(respectMultiSelection: boolean, includeNestedChildren = false): ExplorerItem[] { if (!this.view) { return []; } - return this.view.getContext(respectMultiSelection); + const items = this.view.getContext(respectMultiSelection); + if (includeNestedChildren) { + items.forEach(item => { + const nestedChildren = item.nestedChildren; + if (nestedChildren) { + items.push(...nestedChildren); + } + }); + } + return items; } async applyBulkEdit(edit: ResourceFileEdit[], options: { undoLabel: string; progressLabel: string; confirmBeforeUndo?: boolean; progressLocation?: ProgressLocation.Explorer | ProgressLocation.Window }): Promise { @@ -164,6 +178,7 @@ export class ExplorerService implements IExplorerService { await this.bulkEditService.apply(edit, { undoRedoSource: UNDO_REDO_SOURCE, label: options.undoLabel, + code: 'undoredo.explorerOperation', progress, token: cancellationTokenSource.token, confirmBeforeUndo: options.confirmBeforeUndo @@ -216,7 +231,7 @@ export class ExplorerService implements IExplorerService { } isCut(item: ExplorerItem): boolean { - return !!this.cutItems && this.cutItems.indexOf(item) >= 0; + return !!this.cutItems && this.cutItems.some(i => this.uriIdentityService.extUri.isEqual(i.resource, item.resource)); } getEditable(): { stat: ExplorerItem; data: IEditableData } | undefined { @@ -320,27 +335,30 @@ export class ExplorerService implements IExplorerService { const newElement = e.target; const oldParentResource = dirname(oldResource); const newParentResource = dirname(newElement.resource); + const modelElements = this.model.findAll(oldResource); + const sameParentMove = modelElements.every(e => !e.nestedParent) && this.uriIdentityService.extUri.isEqual(oldParentResource, newParentResource); // Handle Rename - if (this.uriIdentityService.extUri.isEqual(oldParentResource, newParentResource)) { - const modelElements = this.model.findAll(oldResource); - modelElements.forEach(async modelElement => { + if (sameParentMove) { + await Promise.all(modelElements.map(async modelElement => { // Rename File (Model) modelElement.rename(newElement); await this.view?.refresh(false, modelElement.parent); - }); + })); } // Handle Move else { const newParents = this.model.findAll(newParentResource); - const modelElements = this.model.findAll(oldResource); - if (newParents.length && modelElements.length) { // Move in Model await Promise.all(modelElements.map(async (modelElement, index) => { const oldParent = modelElement.parent; + const oldNestedParent = modelElement.nestedParent; modelElement.move(newParents[index]); + if (oldNestedParent) { + await this.view?.refresh(false, oldNestedParent); + } await this.view?.refresh(false, oldParent); await this.view?.refresh(false, newParents[index]); })); @@ -366,7 +384,7 @@ export class ExplorerService implements IExplorerService { private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { let shouldRefresh = false; - if (event?.affectedKeys.some(x => x.startsWith('explorer.experimental.fileNesting.'))) { + if (event?.affectedKeys.some(x => x.startsWith('explorer.fileNesting.'))) { shouldRefresh = true; } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index d77ac057a06..21e91303742 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -11,7 +11,8 @@ import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/ import { ILocalizedString } from 'vs/platform/action/common/action'; import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; -import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { openWindowCommand, newWindowCommand } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, OpenEditorsDirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, OpenEditorsReadonlyEditorContext, OPEN_WITH_EXPLORER_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL, SAVE_ALL_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index ef77619e1d3..f56991447c7 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -18,7 +18,7 @@ import { IQuickInputService, ItemActivation } from 'vs/platform/quickinput/commo import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -906,7 +906,9 @@ export const renameHandler = async (accessor: ServicesAccessor) => { export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true).filter(s => !s.isRoot); + const configurationService = accessor.get(IConfigurationService); + const groupNests = configurationService.getValue().explorer.fileNesting.operateAsGroup; + const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, true); } @@ -914,7 +916,9 @@ export const moveFileToTrashHandler = async (accessor: ServicesAccessor) => { export const deleteFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true).filter(s => !s.isRoot); + const configurationService = accessor.get(IConfigurationService); + const groupNests = configurationService.getValue().explorer.fileNesting.operateAsGroup; + const stats = explorerService.getContext(true, groupNests).filter(s => !s.isRoot); if (stats.length) { await deleteFiles(accessor.get(IExplorerService), accessor.get(IWorkingCopyFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), stats, false); @@ -924,7 +928,9 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { let pasteShouldMove = false; export const copyFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true); + const configurationService = accessor.get(IConfigurationService); + const groupNests = configurationService.getValue().explorer.fileNesting.operateAsGroup; + const stats = explorerService.getContext(true, groupNests); if (stats.length > 0) { await explorerService.setToCopy(stats, false); pasteShouldMove = false; @@ -933,7 +939,9 @@ export const copyFileHandler = async (accessor: ServicesAccessor) => { export const cutFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - const stats = explorerService.getContext(true); + const configurationService = accessor.get(IConfigurationService); + const groupNests = configurationService.getValue().explorer.fileNesting.operateAsGroup; + const stats = explorerService.getContext(true, groupNests); if (stats.length > 0) { await explorerService.setToCopy(stats, true); pasteShouldMove = true; diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index b17db298b7f..2641877c8cd 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -17,7 +17,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IListService } from 'vs/platform/list/browser/listService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IFileService } from 'vs/platform/files/common/files'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; @@ -47,49 +47,7 @@ import { hash } from 'vs/base/common/hash'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; - -// Commands - -export const REVEAL_IN_EXPLORER_COMMAND_ID = 'revealInExplorer'; -export const REVERT_FILE_COMMAND_ID = 'workbench.action.files.revert'; -export const OPEN_TO_SIDE_COMMAND_ID = 'explorer.openToSide'; -export const OPEN_WITH_EXPLORER_COMMAND_ID = 'explorer.openWith'; -export const SELECT_FOR_COMPARE_COMMAND_ID = 'selectForCompare'; - -export const COMPARE_SELECTED_COMMAND_ID = 'compareSelected'; -export const COMPARE_RESOURCE_COMMAND_ID = 'compareFiles'; -export const COMPARE_WITH_SAVED_COMMAND_ID = 'workbench.files.action.compareWithSaved'; -export const COPY_PATH_COMMAND_ID = 'copyFilePath'; -export const COPY_RELATIVE_PATH_COMMAND_ID = 'copyRelativeFilePath'; - -export const SAVE_FILE_AS_COMMAND_ID = 'workbench.action.files.saveAs'; -export const SAVE_FILE_AS_LABEL = nls.localize('saveAs', "Save As..."); -export const SAVE_FILE_COMMAND_ID = 'workbench.action.files.save'; -export const SAVE_FILE_LABEL = nls.localize('save', "Save"); -export const SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID = 'workbench.action.files.saveWithoutFormatting'; -export const SAVE_FILE_WITHOUT_FORMATTING_LABEL = nls.localize('saveWithoutFormatting', "Save without Formatting"); - -export const SAVE_ALL_COMMAND_ID = 'saveAll'; -export const SAVE_ALL_LABEL = nls.localize('saveAll', "Save All"); - -export const SAVE_ALL_IN_GROUP_COMMAND_ID = 'workbench.files.action.saveAllInGroup'; - -export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles'; - -export const OpenEditorsGroupContext = new RawContextKey('groupFocusedInOpenEditors', false); -export const OpenEditorsDirtyEditorContext = new RawContextKey('dirtyEditorFocusedInOpenEditors', false); -export const OpenEditorsReadonlyEditorContext = new RawContextKey('readonlyEditorFocusedInOpenEditors', false); -export const ResourceSelectedForCompareContext = new RawContextKey('resourceSelectedForCompare', false); - -export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; -export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); - -export const PREVIOUS_COMPRESSED_FOLDER = 'previousCompressedFolder'; -export const NEXT_COMPRESSED_FOLDER = 'nextCompressedFolder'; -export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder'; -export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder'; -export const NEW_UNTITLED_FILE_COMMAND_ID = 'workbench.action.files.newUntitledFile'; -export const NEW_UNTITLED_FILE_LABEL = nls.localize('newUntitledFile', "New Untitled File"); +import { OPEN_TO_SIDE_COMMAND_ID, COMPARE_WITH_SAVED_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, COMPARE_SELECTED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, COPY_PATH_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_WITH_EXPLORER_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_ALL_COMMAND_ID, SAVE_ALL_IN_GROUP_COMMAND_ID, SAVE_FILES_COMMAND_ID, REVERT_FILE_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, PREVIOUS_COMPRESSED_FOLDER, NEXT_COMPRESSED_FOLDER, FIRST_COMPRESSED_FOLDER, LAST_COMPRESSED_FOLDER, NEW_UNTITLED_FILE_COMMAND_ID, NEW_UNTITLED_FILE_LABEL } from './fileConstants'; export const openWindowCommand = (accessor: ServicesAccessor, toOpen: IWindowOpenable[], options?: IOpenWindowOptions) => { if (Array.isArray(toOpen)) { diff --git a/src/vs/workbench/contrib/files/browser/fileConstants.ts b/src/vs/workbench/contrib/files/browser/fileConstants.ts new file mode 100644 index 00000000000..4e65b120256 --- /dev/null +++ b/src/vs/workbench/contrib/files/browser/fileConstants.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export const REVEAL_IN_EXPLORER_COMMAND_ID = 'revealInExplorer'; +export const REVERT_FILE_COMMAND_ID = 'workbench.action.files.revert'; +export const OPEN_TO_SIDE_COMMAND_ID = 'explorer.openToSide'; +export const OPEN_WITH_EXPLORER_COMMAND_ID = 'explorer.openWith'; +export const SELECT_FOR_COMPARE_COMMAND_ID = 'selectForCompare'; + +export const COMPARE_SELECTED_COMMAND_ID = 'compareSelected'; +export const COMPARE_RESOURCE_COMMAND_ID = 'compareFiles'; +export const COMPARE_WITH_SAVED_COMMAND_ID = 'workbench.files.action.compareWithSaved'; +export const COPY_PATH_COMMAND_ID = 'copyFilePath'; +export const COPY_RELATIVE_PATH_COMMAND_ID = 'copyRelativeFilePath'; + +export const SAVE_FILE_AS_COMMAND_ID = 'workbench.action.files.saveAs'; +export const SAVE_FILE_AS_LABEL = nls.localize('saveAs', "Save As..."); +export const SAVE_FILE_COMMAND_ID = 'workbench.action.files.save'; +export const SAVE_FILE_LABEL = nls.localize('save', "Save"); +export const SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID = 'workbench.action.files.saveWithoutFormatting'; +export const SAVE_FILE_WITHOUT_FORMATTING_LABEL = nls.localize('saveWithoutFormatting', "Save without Formatting"); + +export const SAVE_ALL_COMMAND_ID = 'saveAll'; +export const SAVE_ALL_LABEL = nls.localize('saveAll', "Save All"); + +export const SAVE_ALL_IN_GROUP_COMMAND_ID = 'workbench.files.action.saveAllInGroup'; + +export const SAVE_FILES_COMMAND_ID = 'workbench.action.files.saveFiles'; + +export const OpenEditorsGroupContext = new RawContextKey('groupFocusedInOpenEditors', false); +export const OpenEditorsDirtyEditorContext = new RawContextKey('dirtyEditorFocusedInOpenEditors', false); +export const OpenEditorsReadonlyEditorContext = new RawContextKey('readonlyEditorFocusedInOpenEditors', false); +export const ResourceSelectedForCompareContext = new RawContextKey('resourceSelectedForCompare', false); + +export const REMOVE_ROOT_FOLDER_COMMAND_ID = 'removeRootFolder'; +export const REMOVE_ROOT_FOLDER_LABEL = nls.localize('removeFolderFromWorkspace', "Remove Folder from Workspace"); + +export const PREVIOUS_COMPRESSED_FOLDER = 'previousCompressedFolder'; +export const NEXT_COMPRESSED_FOLDER = 'nextCompressedFolder'; +export const FIRST_COMPRESSED_FOLDER = 'firstCompressedFolder'; +export const LAST_COMPRESSED_FOLDER = 'lastCompressedFolder'; +export const NEW_UNTITLED_FILE_COMMAND_ID = 'workbench.action.files.newUntitledFile'; +export const NEW_UNTITLED_FILE_LABEL = nls.localize('newUntitledFile', "New Untitled File"); diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 21b615ea6d3..08fdd59d803 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -317,7 +317,8 @@ export class BrowserFileUpload { // Read the file in chunks using File.stream() web APIs try { - const reader: ReadableStreamDefaultReader = file.stream().getReader(); + // TODO@electron: duplicate type definitions originate from `@types/node/stream/consumers.d.ts` + const reader: ReadableStreamDefaultReader = (file.stream() as unknown as ReadableStream).getReader(); let res = await reader.read(); while (!res.done) { @@ -536,11 +537,11 @@ export class ExternalFileImport { const undoLevel = this.configurationService.getValue().explorer.confirmUndo; await this.explorerService.applyBulkEdit(resourceFileEdits, { undoLabel: resourcesFiltered.length === 1 ? - localize('importFile', "Import {0}", basename(resourcesFiltered[0])) : - localize('importnFile', "Import {0} resources", resourcesFiltered.length), + localize({ comment: ['substitution will be the name of the file that was imported'], key: 'importFile' }, "Import {0}", basename(resourcesFiltered[0])) : + localize({ comment: ['substitution will be the number of files that were imported'], key: 'importnFile' }, "Import {0} resources", resourcesFiltered.length), progressLabel: resourcesFiltered.length === 1 ? - localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : - localize('copyingnFile', "Copying {0} resources", resourcesFiltered.length), + localize({ comment: ['substitution will be the name of the file that was copied'], key: 'copyingFile' }, "Copying {0}", basename(resourcesFiltered[0])) : + localize({ comment: ['substitution will be the number of files that were copied'], key: 'copyingnFile' }, "Copying {0} resources", resourcesFiltered.length), progressLocation: ProgressLocation.Window, confirmBeforeUndo: undoLevel === UndoConfirmLevel.Verbose || undoLevel === UndoConfirmLevel.Default, }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index bd3182cd5c9..706fc028bd4 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -266,6 +266,7 @@ configurationRegistry.registerConfiguration({ 'files.maxMemoryForLargeFilesMB': { 'type': 'number', 'default': 4096, + 'minimum': 0, 'markdownDescription': nls.localize('maxMemoryForLargeFilesMB', "Controls the memory available to VS Code after restart when trying to open large files. Same effect as specifying `--max-memory=NEWSIZE` on the command line."), included: isNative }, @@ -332,7 +333,8 @@ configurationRegistry.registerConfiguration({ 'explorer.openEditors.visible': { 'type': 'number', 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "Number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), - 'default': 9 + 'default': 9, + 'minimum': 0 }, 'explorer.openEditors.sortOrder': { 'type': 'string', @@ -404,7 +406,7 @@ configurationRegistry.registerConfiguration({ nls.localize('sortOrder.modified', 'Files and folders are sorted by last modified date in descending order. Folders are displayed before files.'), nls.localize('sortOrder.foldersNestsFiles', 'Files and folders are sorted by their names. Folders are displayed before files. Files with nested children are displayed before other files.') ], - 'description': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.experimental.fileNesting.enabled#` is enabled, also controls sorting of nested files.") + 'markdownDescription': nls.localize('sortOrder', "Controls the property-based sorting of files and folders in the explorer. When `#explorer.experimental.fileNesting.enabled#` is enabled, also controls sorting of nested files.") }, 'explorer.sortOrderLexicographicOptions': { 'type': 'string', @@ -459,33 +461,50 @@ configurationRegistry.registerConfiguration({ 'default': 'auto' }, 'explorer.experimental.fileNesting.enabled': { - 'type': 'boolean', - 'markdownDescription': nls.localize('fileNestingEnabled', "Experimental. Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), - 'default': false, + deprecationMessage: nls.localize('deprecated', "Deprecated, use non-experimental setting names.") }, 'explorer.experimental.fileNesting.expand': { - 'type': 'boolean', - 'markdownDescription': nls.localize('fileNestingExpand', "Experimental. Controls whether file nests are automatically expanded. `#explorer.experimental.fileNesting.enabled#` must be set for this to take effect."), - 'default': true, + deprecationMessage: nls.localize('deprecated', "Deprecated, use non-experimental setting names.") + }, + 'explorer.experimental.fileNesting.operateAsGroup': { + deprecationMessage: nls.localize('deprecated', "Deprecated, use non-experimental setting names.") }, 'explorer.experimental.fileNesting.patterns': { + deprecationMessage: nls.localize('deprecated', "Deprecated, use non-experimental setting names.") + }, + 'explorer.fileNesting.enabled': { + 'type': 'boolean', + 'markdownDescription': nls.localize('fileNestingEnabled', "Controls whether file nesting is enabled in the explorer. File nesting allows for related files in a directory to be visually grouped together under a single parent file."), + 'default': false, + }, + 'explorer.fileNesting.expand': { + 'type': 'boolean', + 'markdownDescription': nls.localize('fileNestingExpand', "Controls whether file nests are automatically expanded."), + 'default': true, + }, + 'explorer.fileNesting.operateAsGroup': { + 'type': 'boolean', + 'markdownDescription': nls.localize('operateAsGroup', "Controls whether file nests are treated as a group for clipboard operations, file deletions, and during drag and drop."), + 'default': false, + }, + 'explorer.fileNesting.patterns': { 'type': 'object', - 'markdownDescription': nls.localize('fileNestingPatterns', "Experimental. Controls nesting of files in the explorer. `#explorer.experimental.fileNesting.enabled#` must be set for this to take effect. Each key describes a parent file pattern and each value should be a comma separated list of children file patterns that will be nested under the parent.\n\nA single `*` in a parent pattern may be used to capture any substring, which can then be matched against using `$\u200b(capture)` in a child pattern. Child patterns may also contain one `*` to match any substring.\n\nFor example, given the configuration `*.ts => $(capture).js, $(capture).*.ts`, and a directory containing `a.ts, a.js, a.d.ts`, and `b.js`, nesting would apply as follows: \n- `*.ts` matches `a.ts`, capturing `a`. This causes any sibilings matching `a.js` or `a.*.ts` to be nested under `a.ts`\n - `a.js` matches `a.js` exactly, so is nested under `a.ts`\n - `a.d.ts` matches `a.*.ts`, so is also nested under `a.ts`\n\nThe final directory will be rendered with `a.ts` containg `a.js` and `a.d.ts` as nested children, and `b.js` as normal file."), + 'markdownDescription': nls.localize('fileNestingPatterns', "Controls nesting of files in the explorer. Each __Item__ represents a parent pattern and may contain a single `*` character that matches any string. Each __Value__ represents a comma separated list of the child patterns that should be shown nested under a given parent. Child patterns may contain several special tokens:\n- `$\u200b(capture)`: Matches the resolved value of the `*` from the parent pattern\n- `$\u200b(basename)`: Matches the parent file's basename, the `file` in `file.ts`\n- `$\u200b(extname)`: Matches the parent file's extension, the `ts` in `file.ts`\n- `$\u200b(dirname)`: Matches the parent file's directory name, the `src` in `src/file.ts`\n- `*`: Matches any string, may only be used once per child pattern"), patternProperties: { '^[^*]*\\*?[^*]*$': { - markdownDescription: nls.localize('fileNesting.description', "Key patterns may contain a single `*` capture group which matches any string. Each value pattern may contain one `$\u200b(capture)` token to be substituted with the parent capture group and one `*` token to match any string"), + markdownDescription: nls.localize('fileNesting.description', "Each key pattern may contain several special sequences: \n- `$\u200b(capture)`: Matches the resolved value of the `*` from the parent pattern\n- `$\u200b(basename)`: Matches the parent file's basename, the `file` in `file.ts`\n- `$\u200b(extname)`: Matches the parent file's extension, the `ts` in `file.ts`\n- `$\u200b(dirname)`: Matches the parent file's directory name, the `src` in `src/file.ts`\n- `*`: Matches any string, may only be used once per child pattern"), type: 'string', pattern: '^([^,*]*\\*?[^,*]*)(, ?[^,*]*\\*?[^,*]*)*$', } }, additionalProperties: false, 'default': { - '*.ts': '$(capture).js, $(capture).*.ts', + '*.ts': '$(capture).js', '*.js': '$(capture).js.map, $(capture).min.js, $(capture).d.ts', '*.jsx': '$(capture).js', '*.tsx': '$(capture).ts', 'tsconfig.json': 'tsconfig.*.json', - 'package.json': 'package-lock.json, .npmrc, yarn.lock, .yarnrc, pnpm-lock.yaml', + 'package.json': 'package-lock.json, yarn.lock', } } } diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index 3baa0f02679..3c034c4e388 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -23,7 +23,7 @@ export interface IExplorerService { readonly roots: ExplorerItem[]; readonly sortOrderConfiguration: ISortOrderConfiguration; - getContext(respectMultiSelection: boolean): ExplorerItem[]; + getContext(respectMultiSelection: boolean, includeNestedChildren?: boolean): ExplorerItem[]; hasViewFocus(): boolean; setEditable(stat: ExplorerItem, data: IEditableData | null): Promise; getEditable(): { stat: ExplorerItem; data: IEditableData } | undefined; diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 29bad547386..4948d9d7f17 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -95,6 +95,7 @@ } /* High Contrast Theming */ -.monaco-workbench.hc-black .explorer-viewlet .explorer-item { +.monaco-workbench.hc-black .explorer-viewlet .explorer-item, +.monaco-workbench.hc-light .explorer-viewlet .explorer-item { line-height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 8d8907f8df4..91288a65d40 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -9,10 +9,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; -import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { ResourcesDropHandler } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -20,6 +20,7 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isWeb } from 'vs/base/common/platform'; +import { DragAndDropObserver } from 'vs/base/browser/dom'; export class EmptyView extends ViewPane { @@ -53,32 +54,28 @@ export class EmptyView extends ViewPane { protected override renderBody(container: HTMLElement): void { super.renderBody(container); - if (!isWeb) { - // Only observe in desktop environments because accessing - // locally dragged files and folders is only possible there - this._register(new DragAndDropObserver(container, { - onDrop: e => { - container.style.backgroundColor = ''; - const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: true }); - dropHandler.handleDrop(e, () => undefined, () => undefined); - }, - onDragEnter: () => { - const color = this.themeService.getColorTheme().getColor(listDropBackground); - container.style.backgroundColor = color ? color.toString() : ''; - }, - onDragEnd: () => { - container.style.backgroundColor = ''; - }, - onDragLeave: () => { - container.style.backgroundColor = ''; - }, - onDragOver: e => { - if (e.dataTransfer) { - e.dataTransfer.dropEffect = 'copy'; - } + this._register(new DragAndDropObserver(container, { + onDrop: e => { + container.style.backgroundColor = ''; + const dropHandler = this.instantiationService.createInstance(ResourcesDropHandler, { allowWorkspaceOpen: !isWeb || isTemporaryWorkspace(this.contextService.getWorkspace()) }); + dropHandler.handleDrop(e, () => undefined, () => undefined); + }, + onDragEnter: () => { + const color = this.themeService.getColorTheme().getColor(listDropBackground); + container.style.backgroundColor = color ? color.toString() : ''; + }, + onDragEnd: () => { + container.style.backgroundColor = ''; + }, + onDragLeave: () => { + container.style.backgroundColor = ''; + }, + onDragOver: e => { + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; } - })); - } + } + })); this.refreshTitle(); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 09ad38d8d62..d01cde745a6 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -384,7 +384,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { const isCompressionEnabled = () => this.configurationService.getValue('explorer.compactFolders'); - const getFileNestingSettings = () => this.configurationService.getValue().explorer.experimental.fileNesting; + const getFileNestingSettings = () => this.configurationService.getValue().explorer.fileNesting; this.tree = >this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'FileExplorer', container, new ExplorerDelegate(), new ExplorerCompressionDelegate(), [this.renderer], this.instantiationService.createInstance(ExplorerDataSource), { @@ -625,7 +625,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { } const toRefresh = item || this.tree.getInput(); - if (this.configurationService.getValue()?.explorer?.experimental?.fileNesting?.enabled) { + if (this.configurationService.getValue().explorer.fileNesting.enabled) { return (async () => { try { await this.tree.updateChildren(toRefresh, recursive, false, { @@ -692,31 +692,18 @@ export class ExplorerView extends ViewPane implements IExplorerView { if (!previousInput && input.length === 1 && this.configurationService.getValue().explorer.expandSingleFolderWorkspaces) { await this.tree.expand(input[0]).catch(() => { }); } - // TODO@jkearl: Hidden & Probably not needed, remove eventaully. - const useOldStyle = this.configurationService.getValue('explorer.legacyWorkspaceFolderExpandMode'); - if (useOldStyle) { - if (Array.isArray(previousInput) && previousInput.length < input.length) { - // Roots added to the explorer -> expand them. - await Promise.all(input.slice(previousInput.length).map(async item => { + if (Array.isArray(previousInput)) { + const previousRoots = new ResourceMap(); + previousInput.forEach(previousRoot => previousRoots.set(previousRoot.resource, true)); + + // Roots added to the explorer -> expand them. + await Promise.all(input.map(async item => { + if (!previousRoots.has(item.resource)) { try { await this.tree.expand(item); } catch (e) { } - })); - } - } else { - if (Array.isArray(previousInput)) { - const previousRoots = new ResourceMap(); - previousInput.forEach(previousRoot => previousRoots.set(previousRoot.resource, true)); - - // Roots added to the explorer -> expand them. - await Promise.all(input.map(async item => { - if (!previousRoots.has(item.resource)) { - try { - await this.tree.expand(item); - } catch (e) { } - } - })); - } + } + })); } } if (initialInputSetup) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index f67e14bc80a..bb78117e40c 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -11,7 +11,7 @@ import { IProgressService, ProgressLocation, } from 'vs/platform/progress/common import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IFileLabelOptions, IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; @@ -56,6 +56,7 @@ import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { BrowserFileUpload, ExternalFileImport, getMultipleFilesOverwriteConfirm } from 'vs/workbench/contrib/files/browser/fileImportExport'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -257,6 +258,7 @@ export interface IFileTemplateData { export class FilesRenderer implements ICompressibleTreeRenderer, IListAccessibilityProvider, IDisposable { static readonly ID = 'file'; + private styler: HTMLStyleElement; private config: IFilesConfiguration; private configListener: IDisposable; private compressedNavigationControllers = new Map(); @@ -275,11 +277,25 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); + + this.styler = DOM.createStyleSheet(); + const buildOffsetStyles = () => { + const indent = this.configurationService.getValue('workbench.tree.indent'); + const offset = Math.max(22 - indent, 0); // derived via inspection + const rule = `.explorer-viewlet .explorer-item.align-nest-icon-with-parent-icon { margin-left: ${offset}px }`; + if (this.styler.innerText !== rule) { this.styler.innerText = rule; } + }; + this.configListener = this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('explorer')) { this.config = this.configurationService.getValue(); } + if (e.affectsConfiguration('workbench.tree.indent')) { + buildOffsetStyles(); + } }); + + buildOffsetStyles(); } getWidgetAriaLabel(): string { @@ -364,28 +380,57 @@ export class FilesRenderer implements ICompressibleTreeRenderer { + // Offset nested children unless folders have both chevrons and icons, otherwise alignment breaks + const theme = this.themeService.getFileIconTheme(); - return templateData.label.onDidRender(() => { + // Hack to always render chevrons for file nests, or else may not be able to identify them. + const twistieContainer = (templateData.container.parentElement?.parentElement?.querySelector('.monaco-tl-twistie') as HTMLElement); + if (twistieContainer) { + if (stat.hasNests && theme.hidesExplorerArrows) { + twistieContainer.classList.add('force-twistie'); + } else { + twistieContainer.classList.remove('force-twistie'); + } + } + + // when explorer arrows are hidden or there are no folder icons, nests get misaligned as they are forced to have arrows and files typically have icons + // Apply some CSS magic to get things looking as reasonable as possible. + const themeIsUnhappyWithNesting = theme.hasFileIcons && (theme.hidesExplorerArrows || !theme.hasFolderIcons); + const realignNestedChildren = stat.nestedParent && themeIsUnhappyWithNesting; + + templateData.label.setResource({ resource: stat.resource, name: label }, { + fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE, + extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses, + fileDecorations: this.config.explorer.decorations, + matches: createMatches(filterData), + separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority), + domId + }); + }; + + elementDisposables.add(this.themeService.onDidFileIconThemeChange(() => setResourceData())); + elementDisposables.add(this.configurationService.onDidChangeConfiguration((e) => + e.affectsConfiguration('explorer.fileNesting.hideIconsToMatchFolders') && setResourceData())); + + setResourceData(); + + elementDisposables.add(templateData.label.onDidRender(() => { try { this.updateWidth(stat); } catch (e) { // noop since the element might no longer be in the tree, no update of width necessary } - }); + })); + + return elementDisposables; } private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable { @@ -394,7 +439,18 @@ export class FilesRenderer implements ICompressibleTreeRenderer { // External file DND (Import/Upload file) if (data instanceof NativeDragAndDropData) { - // Native OS file DND into Web - if (containsDragType(originalEvent, 'Files') && isWeb) { - const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); - await browserUpload.upload(target, originalEvent); - } - // 2 Cases handled for import: - // FS-Provided file DND into Web/Desktop - // Native OS file DND into Desktop - else { + // Use local file import when supported + if (!isWeb || (isTemporaryWorkspace(this.contextService.getWorkspace()) && WebFileSystemAccess.supported(window))) { const fileImport = this.instantiationService.createInstance(ExternalFileImport); await fileImport.import(resolvedTarget, originalEvent); } + // Otherwise fallback to browser based file upload + else { + const browserUpload = this.instantiationService.createInstance(BrowserFileUpload); + await browserUpload.upload(target, originalEvent); + } } // In-Explorer DND (Move/Copy file) @@ -1029,6 +1084,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private async handleExplorerDrop(data: ElementsDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const elementsData = FileDragAndDrop.getStatsFromDragAndDropData(data); const items = distinctParents(elementsData, s => s.resource); + + if (this.configurationService.getValue().explorer.fileNesting.operateAsGroup) { + for (const item of items) { + const nestedChildren = item.nestedChildren; + if (nestedChildren) { + items.push(...nestedChildren); + } + } + } const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); // Handle confirm setting diff --git a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css index 45b8572d35c..b4542a2b765 100644 --- a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css +++ b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css @@ -80,6 +80,8 @@ } .monaco-workbench.hc-black .open-editors .open-editor, -.monaco-workbench.hc-black .open-editors .editor-group { +.monaco-workbench.hc-black .open-editors .editor-group, +.monaco-workbench.hc-light .open-editors .open-editor, +.monaco-workbench.hc-light .open-editors .editor-group { line-height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 962cdff6e9f..b7bc23b3257 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -30,7 +30,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; -import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { ResourcesDropHandler, fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -39,7 +39,6 @@ import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { isWeb } from 'vs/base/common/platform'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; @@ -695,10 +694,6 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop> { + private getAttributes(filename: string, dirname: string): FilenameAttributes { + const lastDot = filename.lastIndexOf('.'); + if (lastDot < 1) { + return { + dirname, + basename: filename, + extname: '' + }; + } else { + return { + dirname, + basename: filename.substring(0, lastDot), + extname: filename.substring(lastDot + 1) + }; + } + } + + nest(files: string[], dirname: string): Map> { const parentFinder = new PreTrie(); for (const potentialParent of files) { - const children = this.root.get(potentialParent); + const attributes = this.getAttributes(potentialParent, dirname); + const children = this.root.get(potentialParent, attributes); for (const child of children) { parentFinder.add(child, potentialParent); } @@ -48,8 +75,8 @@ export class ExplorerFileNestingTrie { const findAllRootAncestors = (file: string, seen: Set = new Set()): string[] => { if (seen.has(file)) { return []; } seen.add(file); - - const ancestors = parentFinder.get(file); + const attributes = this.getAttributes(file, dirname); + const ancestors = parentFinder.get(file, attributes); if (ancestors.length === 0) { return [file]; } @@ -101,15 +128,15 @@ export class PreTrie { } } - get(key: string): string[] { + get(key: string, attributes: FilenameAttributes): string[] { const results: string[] = []; - results.push(...this.value.get(key)); + results.push(...this.value.get(key, attributes)); const head = key[0]; const rest = key.slice(1); const existing = this.map.get(head); if (existing) { - results.push(...existing.get(rest)); + results.push(...existing.get(rest, attributes)); } return results; @@ -128,8 +155,8 @@ export class PreTrie { /** Export for test only. */ export class SufTrie { - private star: string[] = []; - private epsilon: string[] = []; + private star: SubstitutionString[] = []; + private epsilon: SubstitutionString[] = []; private map: Map = new Map(); hasItems: boolean = false; @@ -139,9 +166,9 @@ export class SufTrie { add(key: string, value: string) { this.hasItems = true; if (key === '*') { - this.star.push(value); + this.star.push(new SubstitutionString(value)); } else if (key === '') { - this.epsilon.push(value); + this.epsilon.push(new SubstitutionString(value)); } else { const tail = key[key.length - 1]; const rest = key.slice(0, key.length - 1); @@ -157,20 +184,20 @@ export class SufTrie { } } - get(key: string): string[] { + get(key: string, attributes: FilenameAttributes): string[] { const results: string[] = []; if (key === '') { - results.push(...this.epsilon); + results.push(...this.epsilon.map(ss => ss.substitute(attributes))); } if (this.star.length) { - results.push(...this.star.map(x => x.replace('$(capture)', key))); + results.push(...this.star.map(ss => ss.substitute(attributes, key))); } const tail = key[key.length - 1]; const rest = key.slice(0, key.length - 1); const existing = this.map.get(tail); if (existing) { - results.push(...existing.get(rest)); + results.push(...existing.get(rest, attributes)); } return results; @@ -193,3 +220,56 @@ export class SufTrie { return lines.map(l => indentation + l).join('\n'); } } + +const enum SubstitutionType { + capture = 'capture', + basename = 'basename', + dirname = 'dirname', + extname = 'extname', +} + +const substitutionStringTokenizer = /\$\((capture|basename|dirname|extname)\)/g; + +class SubstitutionString { + + private tokens: (string | { capture: SubstitutionType })[] = []; + + constructor(pattern: string) { + substitutionStringTokenizer.lastIndex = 0; + let token; + let lastIndex = 0; + while (token = substitutionStringTokenizer.exec(pattern)) { + const prefix = pattern.slice(lastIndex, token.index); + this.tokens.push(prefix); + + const type = token[1]; + switch (type) { + case SubstitutionType.basename: + case SubstitutionType.dirname: + case SubstitutionType.extname: + case SubstitutionType.capture: + this.tokens.push({ capture: type }); + break; + default: throw Error('unknown substitution type: ' + type); + } + lastIndex = token.index + token[0].length; + } + + if (lastIndex !== pattern.length) { + const suffix = pattern.slice(lastIndex, pattern.length); + this.tokens.push(suffix); + } + } + + substitute(attributes: FilenameAttributes, capture?: string): string { + return this.tokens.map(t => { + if (typeof t === 'string') { return t; } + switch (t.capture) { + case SubstitutionType.basename: return attributes.basename; + case SubstitutionType.dirname: return attributes.dirname; + case SubstitutionType.extname: return attributes.extname; + case SubstitutionType.capture: return capture || ''; + } + }).join(''); + } +} diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index 8b284ddcb1d..6efd1a976eb 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -87,7 +87,8 @@ export class ExplorerItem { public isError = false; private _isExcluded = false; - private nestedChildren: ExplorerItem[] | undefined; + public nestedParent: ExplorerItem | undefined; + public nestedChildren: ExplorerItem[] | undefined; constructor( public resource: URI, @@ -297,7 +298,7 @@ export class ExplorerItem { } fetchChildren(sortOrder: SortOrder): ExplorerItem[] | Promise { - const nestingConfig = this.configService.getValue().explorer.experimental.fileNesting; + const nestingConfig = this.configService.getValue().explorer.fileNesting; // fast path when the children can be resolved sync if (nestingConfig.enabled && this.nestedChildren) { return this.nestedChildren; } @@ -324,6 +325,7 @@ export class ExplorerItem { const fileChildren: [string, ExplorerItem][] = []; const dirChildren: [string, ExplorerItem][] = []; for (const child of this.children.entries()) { + child[1].nestedParent = undefined; if (child[1].isDirectory) { dirChildren.push(child); } else { @@ -331,14 +333,18 @@ export class ExplorerItem { } } - const nested = this.buildFileNester().nest(fileChildren.map(([name]) => name)); + const nested = this.fileNester.nest( + fileChildren.map(([name]) => name), + this.getPlatformAwareName(this.name)); for (const [fileEntryName, fileEntryItem] of fileChildren) { const nestedItems = nested.get(fileEntryName); if (nestedItems !== undefined) { fileEntryItem.nestedChildren = []; for (const name of nestedItems.keys()) { - fileEntryItem.nestedChildren.push(assertIsDefined(this.children.get(name))); + const child = assertIsDefined(this.children.get(name)); + fileEntryItem.nestedChildren.push(child); + child.nestedParent = fileEntryItem; } items.push(fileEntryItem); } else { @@ -358,16 +364,19 @@ export class ExplorerItem { })(); } - // TODO:@jkearl, share one nester across all explorer items and only build on config change - private buildFileNester(): ExplorerFileNestingTrie { - const nestingConfig = this.configService.getValue().explorer.experimental.fileNesting; - const patterns = Object.entries(nestingConfig.patterns) - .filter(entry => - typeof (entry[0]) === 'string' && typeof (entry[1]) === 'string' && entry[0] && entry[1]) - .map(([parentPattern, childrenPatterns]) => - [parentPattern.trim(), childrenPatterns.split(',').map(p => p.trim())] as [string, string[]]); + private _fileNester: ExplorerFileNestingTrie | undefined; + private get fileNester(): ExplorerFileNestingTrie { + if (!this.root._fileNester) { + const nestingConfig = this.configService.getValue({ resource: this.root.resource }).explorer.fileNesting; + const patterns = Object.entries(nestingConfig.patterns) + .filter(entry => + typeof (entry[0]) === 'string' && typeof (entry[1]) === 'string' && entry[0] && entry[1]) + .map(([parentPattern, childrenPatterns]) => + [parentPattern.trim(), childrenPatterns.split(',').map(p => this.getPlatformAwareName(p.trim().replace(/\u200b/g, '')))] as [string, string[]]); - return new ExplorerFileNestingTrie(patterns); + this.root._fileNester = new ExplorerFileNestingTrie(patterns); + } + return this.root._fileNester; } /** @@ -380,6 +389,7 @@ export class ExplorerItem { forgetChildren(): void { this.children.clear(); this._isDirectoryResolved = false; + this._fileNester = undefined; } private getPlatformAwareName(name: string): string { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 95ec6454ad0..b5f86c1ea49 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -98,12 +98,12 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb badges: boolean; }; incrementalNaming: 'simple' | 'smart'; - experimental: { - fileNesting: { - enabled: boolean; - expand: boolean; - patterns: { [parent: string]: string }; - }; + fileNesting: { + enabled: boolean; + operateAsGroup: boolean; + expand: boolean; + hideIconsToMatchFolders: boolean; + patterns: { [parent: string]: string }; }; }; editor: IEditorOptions; diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index a7d49c574cb..7e5450675c1 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -25,7 +25,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); -const REVEAL_IN_OS_WHEN_CONTEXT = ContextKeyExpr.or(ResourceContextKey.Scheme.isEqualTo(Schemas.file), ResourceContextKey.Scheme.isEqualTo(Schemas.userData)); +const REVEAL_IN_OS_WHEN_CONTEXT = ContextKeyExpr.or(ResourceContextKey.Scheme.isEqualTo(Schemas.file), ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)); KeybindingsRegistry.registerCommandAndKeybindingRule({ id: REVEAL_IN_OS_COMMAND_ID, diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts index c0618077d00..b6afa32e4ef 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts @@ -14,7 +14,7 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; export function revealResourcesInOS(resources: URI[], nativeHostService: INativeHostService, workspaceContextService: IWorkspaceContextService): void { if (resources.length) { sequence(resources.map(r => async () => { - if (r.scheme === Schemas.file || r.scheme === Schemas.userData) { + if (r.scheme === Schemas.file || r.scheme === Schemas.vscodeUserData) { nativeHostService.showItemInFolder(r.fsPath); } })); diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index fcf2801010b..e4d1eee0b33 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -26,6 +26,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** * An implementation of editor for file system resources. @@ -49,9 +50,10 @@ export class NativeTextFileEditor extends TextFileEditor { @IExplorerService explorerService: IExplorerService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IProductService private readonly productService: IProductService, - @IPathService pathService: IPathService + @IPathService pathService: IPathService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(telemetryService, fileService, paneCompositeService, instantiationService, contextService, storageService, textResourceConfigurationService, editorService, themeService, editorGroupService, textFileService, explorerService, uriIdentityService, pathService); + super(telemetryService, fileService, paneCompositeService, instantiationService, contextService, storageService, textResourceConfigurationService, editorService, themeService, editorGroupService, textFileService, explorerService, uriIdentityService, pathService, configurationService); } protected override handleSetInputError(error: Error, input: FileEditorInput, options: ITextEditorOptions | undefined): Promise { diff --git a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts index a1bdc151838..091095d421f 100644 --- a/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/explorerFileNestingTrie.test.ts @@ -5,33 +5,35 @@ import { PreTrie, ExplorerFileNestingTrie, SufTrie } from 'vs/workbench/contrib/files/common/explorerFileNestingTrie'; import * as assert from 'assert'; +const fakeFilenameAttributes = { dirname: 'mydir', basename: '', extname: '' }; + suite('SufTrie', () => { test('exactMatches', () => { const t = new SufTrie(); t.add('.npmrc', 'MyKey'); - assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), []); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), []); }); test('starMatches', () => { const t = new SufTrie(); t.add('*.npmrc', 'MyKey'); - assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['MyKey']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['MyKey']); }); test('starSubstitutes', () => { const t = new SufTrie(); t.add('*.npmrc', '$(capture).json'); - assert.deepStrictEqual(t.get('.npmrc'), ['.json']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), ['a.json']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['a.b.c.d.json']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['.json']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['a.json']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['a.b.c.d.json']); }); test('multiMatches', () => { @@ -39,13 +41,13 @@ suite('SufTrie', () => { t.add('*.npmrc', 'Key1'); t.add('*.json', 'Key2'); t.add('*d.npmrc', 'Key3'); - assert.deepStrictEqual(t.get('.npmrc'), ['Key1']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('.json'), ['Key2']); - assert.deepStrictEqual(t.get('a.json'), ['Key2']); - assert.deepStrictEqual(t.get('a.npmrc'), ['Key1']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1', 'Key3']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['Key1']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.json', fakeFilenameAttributes), ['Key2']); + assert.deepStrictEqual(t.get('a.json', fakeFilenameAttributes), ['Key2']); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['Key1']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['Key1', 'Key3']); }); test('multiSubstitutes', () => { @@ -53,14 +55,14 @@ suite('SufTrie', () => { t.add('*.npmrc', 'Key1.$(capture).js'); t.add('*.json', 'Key2.$(capture).js'); t.add('*d.npmrc', 'Key3.$(capture).js'); - assert.deepStrictEqual(t.get('.npmrc'), ['Key1..js']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('.json'), ['Key2..js']); - assert.deepStrictEqual(t.get('a.json'), ['Key2.a.js']); - assert.deepStrictEqual(t.get('a.npmrc'), ['Key1.a.js']); - assert.deepStrictEqual(t.get('a.b.cd.npmrc'), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['Key1..js']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.json', fakeFilenameAttributes), ['Key2..js']); + assert.deepStrictEqual(t.get('a.json', fakeFilenameAttributes), ['Key2.a.js']); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['Key1.a.js']); + assert.deepStrictEqual(t.get('a.b.cd.npmrc', fakeFilenameAttributes), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']); }); }); @@ -68,29 +70,29 @@ suite('PreTrie', () => { test('exactMatches', () => { const t = new PreTrie(); t.add('.npmrc', 'MyKey'); - assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), []); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), []); }); test('starMatches', () => { const t = new PreTrie(); t.add('*.npmrc', 'MyKey'); - assert.deepStrictEqual(t.get('.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), ['MyKey']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['MyKey']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['MyKey']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['MyKey']); }); test('starSubstitutes', () => { const t = new PreTrie(); t.add('*.npmrc', '$(capture).json'); - assert.deepStrictEqual(t.get('.npmrc'), ['.json']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('a.npmrc'), ['a.json']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['a.b.c.d.json']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['.json']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['a.json']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['a.b.c.d.json']); }); test('multiMatches', () => { @@ -98,13 +100,13 @@ suite('PreTrie', () => { t.add('*.npmrc', 'Key1'); t.add('*.json', 'Key2'); t.add('*d.npmrc', 'Key3'); - assert.deepStrictEqual(t.get('.npmrc'), ['Key1']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('.json'), ['Key2']); - assert.deepStrictEqual(t.get('a.json'), ['Key2']); - assert.deepStrictEqual(t.get('a.npmrc'), ['Key1']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1', 'Key3']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['Key1']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.json', fakeFilenameAttributes), ['Key2']); + assert.deepStrictEqual(t.get('a.json', fakeFilenameAttributes), ['Key2']); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['Key1']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['Key1', 'Key3']); }); test('multiSubstitutes', () => { @@ -112,23 +114,23 @@ suite('PreTrie', () => { t.add('*.npmrc', 'Key1.$(capture).js'); t.add('*.json', 'Key2.$(capture).js'); t.add('*d.npmrc', 'Key3.$(capture).js'); - assert.deepStrictEqual(t.get('.npmrc'), ['Key1..js']); - assert.deepStrictEqual(t.get('npmrc'), []); - assert.deepStrictEqual(t.get('.npmrcs'), []); - assert.deepStrictEqual(t.get('.json'), ['Key2..js']); - assert.deepStrictEqual(t.get('a.json'), ['Key2.a.js']); - assert.deepStrictEqual(t.get('a.npmrc'), ['Key1.a.js']); - assert.deepStrictEqual(t.get('a.b.cd.npmrc'), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']); - assert.deepStrictEqual(t.get('a.b.c.d.npmrc'), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']); + assert.deepStrictEqual(t.get('.npmrc', fakeFilenameAttributes), ['Key1..js']); + assert.deepStrictEqual(t.get('npmrc', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.npmrcs', fakeFilenameAttributes), []); + assert.deepStrictEqual(t.get('.json', fakeFilenameAttributes), ['Key2..js']); + assert.deepStrictEqual(t.get('a.json', fakeFilenameAttributes), ['Key2.a.js']); + assert.deepStrictEqual(t.get('a.npmrc', fakeFilenameAttributes), ['Key1.a.js']); + assert.deepStrictEqual(t.get('a.b.cd.npmrc', fakeFilenameAttributes), ['Key1.a.b.cd.js', 'Key3.a.b.c.js']); + assert.deepStrictEqual(t.get('a.b.c.d.npmrc', fakeFilenameAttributes), ['Key1.a.b.c.d.js', 'Key3.a.b.c..js']); }); test('emptyMatches', () => { const t = new PreTrie(); t.add('package*json', 'package'); - assert.deepStrictEqual(t.get('package.json'), ['package']); - assert.deepStrictEqual(t.get('packagejson'), ['package']); - assert.deepStrictEqual(t.get('package-lock.json'), ['package']); + assert.deepStrictEqual(t.get('package.json', fakeFilenameAttributes), ['package']); + assert.deepStrictEqual(t.get('packagejson', fakeFilenameAttributes), ['package']); + assert.deepStrictEqual(t.get('package-lock.json', fakeFilenameAttributes), ['package']); }); }); @@ -165,7 +167,7 @@ suite('StarTrie', () => { 'beep.boop.test1', 'beep.boop.test2', 'beep.boop.a', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'file': ['file.json'], 'boop.test': ['boop.test.1'], @@ -191,7 +193,7 @@ suite('StarTrie', () => { 'c.map', 'd.ts', 'd.map', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'a.ts': ['a.js'], 'ab.js': [], @@ -223,7 +225,7 @@ suite('StarTrie', () => { 'd.aa', 'd.cc', 'e.aa', 'e.bb', 'e.dd', 'e.ee', 'f.aa', 'f.bb', 'f.cc', 'f.dd', 'f.ee', - ]); + ], 'mydir'); assertMapEquals(nesting, { '.a': [], '.b': [], '.c': [], '.d': [], @@ -248,7 +250,7 @@ suite('StarTrie', () => { 'a.js', 'b.js', 'b-vscdoc.js', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'a-vsdoc.js': ['a.js'], @@ -267,7 +269,7 @@ suite('StarTrie', () => { 'a.js', 'b.js', 'vscdoc-b.js', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'vsdoc-a.js': ['a.js'], @@ -286,7 +288,7 @@ suite('StarTrie', () => { 'a.js', 'b.js', 'bib-b-bap.js', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'foo-a-bar.js': ['a.js'], @@ -304,7 +306,7 @@ suite('StarTrie', () => { 'foo.test.js', 'fooTest.js', 'bar.js.js', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'foo.js': ['foo.test.js'], @@ -323,7 +325,7 @@ suite('StarTrie', () => { 'package.json', '.npmrc', 'npm-shrinkwrap.json', 'yarn.lock', '.bowerrc', - ]); + ], 'mydir'); assertMapEquals(nesting, { 'package.json': [ @@ -340,7 +342,7 @@ suite('StarTrie', () => { const nesting1 = t.nest([ '.eslintrc.json', '.eslintignore', - ]); + ], 'mydir'); assertMapEquals(nesting1, { '.eslintrc.json': ['.eslintignore'], @@ -349,13 +351,86 @@ suite('StarTrie', () => { const nesting2 = t.nest([ '.eslintrc', '.eslintignore', - ]); + ], 'mydir'); assertMapEquals(nesting2, { '.eslintrc': ['.eslintignore'], }); }); + test('basename expansion', () => { + const t = new ExplorerFileNestingTrie([ + ['*-vsdoc.js', ['$(basename).doc']], + ]); + + const nesting1 = t.nest([ + 'boop-vsdoc.js', + 'boop-vsdoc.doc', + 'boop.doc', + ], 'mydir'); + + assertMapEquals(nesting1, { + 'boop-vsdoc.js': ['boop-vsdoc.doc'], + 'boop.doc': [], + }); + }); + + test('extname expansion', () => { + const t = new ExplorerFileNestingTrie([ + ['*-vsdoc.js', ['$(extname).doc']], + ]); + + const nesting1 = t.nest([ + 'boop-vsdoc.js', + 'js.doc', + 'boop.doc', + ], 'mydir'); + + assertMapEquals(nesting1, { + 'boop-vsdoc.js': ['js.doc'], + 'boop.doc': [], + }); + }); + + test('added segment matcher', () => { + const t = new ExplorerFileNestingTrie([ + ['*', ['$(basename).*.$(extname)']], + ]); + + const nesting1 = t.nest([ + 'some.file', + 'some.html.file', + 'some.html.nested.file', + 'other.file', + 'some.thing', + 'some.thing.else', + ], 'mydir'); + + assertMapEquals(nesting1, { + 'some.file': ['some.html.file', 'some.html.nested.file'], + 'other.file': [], + 'some.thing': [], + 'some.thing.else': [], + }); + }); + + test('dirname matching', () => { + const t = new ExplorerFileNestingTrie([ + ['index.ts', ['$(dirname).ts']], + ]); + + const nesting1 = t.nest([ + 'otherFile.ts', + 'MyComponent.ts', + 'index.ts', + ], 'MyComponent'); + + assertMapEquals(nesting1, { + 'index.ts': ['MyComponent.ts'], + 'otherFile.ts': [], + }); + }); + test.skip('is fast', () => { const bigNester = new ExplorerFileNestingTrie([ ['*', ['$(capture).*']], @@ -401,7 +476,7 @@ suite('StarTrie', () => { const start = performance.now(); // const _bigResult = - bigNester.nest(bigFiles); + bigNester.nest(bigFiles, 'mydir'); const end = performance.now(); assert(end - start < 1000, 'too slow...' + (end - start)); // console.log(bigResult) diff --git a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts index cb3332a8abb..5559096223e 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactive.contribution.ts @@ -644,11 +644,52 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'interactive.input.focus', + title: { value: localize('interactive.input.focus', "Focus input editor in the interactive window"), original: 'Focus input editor in the interactive window' }, + category: 'Interactive', + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorControl = editorService.activeEditorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget } | undefined; + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + editorService.activeEditorPane?.focus(); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'interactive.history.focus', + title: { value: localize('interactive.history.focus', "Focus history in the interactive window"), original: 'Focus input editor in the interactive window' }, + category: 'Interactive', + f1: false + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorControl = editorService.activeEditorPane?.getControl() as { notebookEditor: NotebookEditorWidget | undefined; codeEditor: CodeEditorWidget; focusHistory: () => void } | undefined; + + if (editorControl && editorControl.notebookEditor && editorControl.codeEditor) { + editorControl.notebookEditor.focus(); + } + } +}); + registerThemingParticipant((theme) => { registerColor('interactive.activeCodeBorder', { dark: theme.getColor(peekViewBorder) ?? '#007acc', light: theme.getColor(peekViewBorder) ?? '#007acc', - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('interactive.activeCodeBorder', 'The border color for the current interactive code cell when the editor has focus.')); // registerColor('interactive.activeCodeBackground', { @@ -660,7 +701,8 @@ registerThemingParticipant((theme) => { registerColor('interactive.inactiveCodeBorder', { dark: theme.getColor(listInactiveSelectionBackground) ?? transparent(listInactiveSelectionBackground, 1), light: theme.getColor(listInactiveSelectionBackground) ?? transparent(listInactiveSelectionBackground, 1), - hc: PANEL_BORDER + hcDark: PANEL_BORDER, + hcLight: PANEL_BORDER }, localize('interactive.inactiveCodeBorder', 'The border color for the current interactive code cell when the editor does not have focus.')); // registerColor('interactive.inactiveCodeBackground', { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index a10a179b3de..99d8c6897b6 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -153,11 +153,11 @@ export class InteractiveEditor extends EditorPane { this._register(this.#keybindingService.onDidUpdateKeybindings(this.#updateInputDecoration, this)); } - private get _inputCellContainerHeight() { + get #inputCellContainerHeight() { return 19 + 2 + INPUT_CELL_VERTICAL_PADDING * 2 + INPUT_EDITOR_PADDING * 2; } - private get _inputCellEditorHeight() { + get #inputCellEditorHeight() { return 19 + INPUT_EDITOR_PADDING * 2; } @@ -167,7 +167,7 @@ export class InteractiveEditor extends EditorPane { 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.#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); @@ -249,7 +249,7 @@ export class InteractiveEditor extends EditorPane { } override saveState(): void { - this._saveEditorViewState(this.input); + this.#saveEditorViewState(this.input); super.saveState(); } @@ -259,11 +259,11 @@ export class InteractiveEditor extends EditorPane { return undefined; } - this._saveEditorViewState(input); - return this._loadNotebookEditorViewState(input); + this.#saveEditorViewState(input); + return this.#loadNotebookEditorViewState(input); } - private _saveEditorViewState(input: EditorInput | undefined): void { + #saveEditorViewState(input: EditorInput | undefined): void { if (this.group && this.#notebookWidget.value && input instanceof InteractiveEditorInput) { if (this.#notebookWidget.value.isDisposed) { return; @@ -278,7 +278,7 @@ export class InteractiveEditor extends EditorPane { } } - private _loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { + #loadNotebookEditorViewState(input: InteractiveEditorInput): InteractiveEditorViewState | undefined { let result: InteractiveEditorViewState | undefined; if (this.group) { result = this.#editorMemento.loadEditorState(this.group, input.notebookEditorInput.resource); @@ -370,17 +370,17 @@ export class InteractiveEditor extends EditorPane { }); if (this.#dimension) { - this.#notebookEditorContainer.style.height = `${this.#dimension.height - this._inputCellContainerHeight}px`; - this.#notebookWidget.value!.layout(this.#dimension.with(this.#dimension.width, this.#dimension.height - this._inputCellContainerHeight), this.#notebookEditorContainer); + this.#notebookEditorContainer.style.height = `${this.#dimension.height - this.#inputCellContainerHeight}px`; + this.#notebookWidget.value!.layout(this.#dimension.with(this.#dimension.width, this.#dimension.height - this.#inputCellContainerHeight), this.#notebookEditorContainer); const { codeCellLeftMargin, cellRunGutter } = this.#notebookOptions.getLayoutConfiguration(); const leftMargin = codeCellLeftMargin + cellRunGutter; - const maxHeight = Math.min(this.#dimension.height / 2, this._inputCellEditorHeight); + const maxHeight = Math.min(this.#dimension.height / 2, this.#inputCellEditorHeight); this.#codeEditorWidget.layout(this.#validateDimension(this.#dimension.width - leftMargin - INPUT_CELL_HORIZONTAL_PADDING_RIGHT, maxHeight)); - this.#inputFocusIndicator.style.height = `${this._inputCellEditorHeight}px`; - this.#inputCellContainer.style.top = `${this.#dimension.height - this._inputCellContainerHeight}px`; + this.#inputFocusIndicator.style.height = `${this.#inputCellEditorHeight}px`; + this.#inputCellContainer.style.top = `${this.#dimension.height - this.#inputCellContainerHeight}px`; this.#inputCellContainer.style.width = `${this.#dimension.width}px`; } @@ -393,7 +393,7 @@ export class InteractiveEditor extends EditorPane { this.#notebookWidget.value?.setParentContextKeyService(this.#contextKeyService); - const viewState = options?.viewState ?? this._loadNotebookEditorViewState(input); + const viewState = options?.viewState ?? this.#loadNotebookEditorViewState(input); await this.#notebookWidget.value!.setModel(model.notebook, viewState?.notebook); model.notebook.setCellCollapseDefault(this.#notebookOptions.getCellCollapseDefault()); this.#notebookWidget.value!.setOptions({ @@ -621,12 +621,12 @@ export class InteractiveEditor extends EditorPane { return; } - this.#notebookEditorContainer.style.height = `${this.#dimension.height - this._inputCellContainerHeight}px`; + this.#notebookEditorContainer.style.height = `${this.#dimension.height - this.#inputCellContainerHeight}px`; this.#layoutWidgets(dimension); } #layoutWidgets(dimension: DOM.Dimension) { - const contentHeight = this.#codeEditorWidget.hasModel() ? this.#codeEditorWidget.getContentHeight() : this._inputCellEditorHeight; + const contentHeight = this.#codeEditorWidget.hasModel() ? this.#codeEditorWidget.getContentHeight() : this.#inputCellEditorHeight; const maxHeight = Math.min(dimension.height / 2, contentHeight); const { codeCellLeftMargin, @@ -689,15 +689,19 @@ export class InteractiveEditor extends EditorPane { this.#codeEditorWidget.focus(); } + focusHistory() { + this.#notebookWidget.value!.focus(); + } + override setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { super.setEditorVisible(visible, group); if (group) { this.#groupListener.clear(); - this.#groupListener.add(group.onWillCloseEditor(e => this._saveEditorViewState(e.editor))); + this.#groupListener.add(group.onWillCloseEditor(e => this.#saveEditorViewState(e.editor))); } if (!visible) { - this._saveEditorViewState(this.input); + this.#saveEditorViewState(this.input); if (this.input && this.#notebookWidget.value) { this.#notebookWidget.value.onWillHide(); } @@ -706,7 +710,7 @@ export class InteractiveEditor extends EditorPane { override clearInput() { if (this.#notebookWidget.value) { - this._saveEditorViewState(this.input); + this.#saveEditorViewState(this.input); this.#notebookWidget.value.onWillHide(); } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index f794d0c06ac..d79b71400e5 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -164,9 +164,10 @@ class EditorStatusContribution implements IWorkbenchContribution { const ariaLabels: string[] = []; const element = document.createElement('div'); for (const status of model.combined) { - element.appendChild(this._renderStatus(status, showSeverity, model.dedicated.includes(status), this._renderDisposables)); + const isPinned = model.dedicated.includes(status); + element.appendChild(this._renderStatus(status, showSeverity, isPinned, this._renderDisposables)); ariaLabels.push(this._asAriaLabel(status)); - isOneBusy = isOneBusy || status.busy; + isOneBusy = isOneBusy || (!isPinned && status.busy); // unpinned items contribute to the busy-indicator of the composite status item } const props: IStatusbarEntry = { name: localize('langStatus.name', "Editor Language Status"), diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistory.contribution.ts b/src/vs/workbench/contrib/localHistory/browser/localHistory.contribution.ts new file mode 100644 index 00000000000..dda52a6f3a5 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/browser/localHistory.contribution.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/workbench/contrib/localHistory/browser/localHistoryCommands'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { LocalHistoryTimeline } from 'vs/workbench/contrib/localHistory/browser/localHistoryTimeline'; + +// Register Local History Timeline +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LocalHistoryTimeline, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistory.ts b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts new file mode 100644 index 00000000000..c182f0ccfd1 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/browser/localHistory.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Codicon } from 'vs/base/common/codicons'; +import { language } from 'vs/base/common/platform'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; + +export const LOCAL_HISTORY_DATE_FORMATTER = new Intl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + +export const LOCAL_HISTORY_MENU_CONTEXT_VALUE = 'localHistory:item'; +export const LOCAL_HISTORY_MENU_CONTEXT_KEY = ContextKeyExpr.equals('timelineItem', LOCAL_HISTORY_MENU_CONTEXT_VALUE); + +export const LOCAL_HISTORY_ICON_ENTRY = registerIcon('localHistory-icon', Codicon.circleOutline, localize('localHistoryIcon', "Icon for a local history entry in the timeline view.")); +export const LOCAL_HISTORY_ICON_RESTORE = registerIcon('localHistory-restore', Codicon.check, localize('localHistoryRestore', "Icon for restoring contents of a local history entry.")); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts new file mode 100644 index 00000000000..f614c226968 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryCommands.ts @@ -0,0 +1,587 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { LocalHistoryFileSystemProvider } from 'vs/workbench/contrib/localHistory/browser/localHistoryFileSystemProvider'; +import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { basename, basenameOrAuthority, dirname } from 'vs/base/common/resources'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { SaveSourceRegistry, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { IModelService } from 'vs/editor/common/services/model'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { firstOrDefault } from 'vs/base/common/arrays'; +import { LOCAL_HISTORY_DATE_FORMATTER, LOCAL_HISTORY_ICON_RESTORE, LOCAL_HISTORY_MENU_CONTEXT_KEY } from 'vs/workbench/contrib/localHistory/browser/localHistory'; + +const LOCAL_HISTORY_CATEGORY = { value: localize('localHistory.category', "Local History"), original: 'Local History' }; + +export interface ITimelineCommandArgument { + uri: URI; + handle: string; +} + +//#region Compare with File + +export const COMPARE_WITH_FILE_LABEL = { value: localize('localHistory.compareWithFile', "Compare with File"), original: 'Compare with File' }; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.compareWithFile', + title: COMPARE_WITH_FILE_LABEL, + menu: { + id: MenuId.TimelineItemContext, + group: '1_compare', + order: 1, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const commandService = accessor.get(ICommandService); + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(entry, entry.workingCopy.resource)); + } + } +}); + +//#endregion + +//#region Compare with Previous + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.compareWithPrevious', + title: { value: localize('localHistory.compareWithPrevious', "Compare with Previous"), original: 'Compare with Previous' }, + menu: { + id: MenuId.TimelineItemContext, + group: '1_compare', + order: 2, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const commandService = accessor.get(ICommandService); + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const editorService = accessor.get(IEditorService); + + const { entry, previous } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + + // Without a previous entry, just show the entry directly + if (!previous) { + return openEntry(entry, editorService); + } + + // Open real diff editor + return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(previous, entry)); + } + } +}); + +//#endregion + +//#region Select for Compare / Compare with Selected + +let itemSelectedForCompare: ITimelineCommandArgument | undefined = undefined; + +const LocalHistoryItemSelectedForCompare = new RawContextKey('localHistoryItemSelectedForCompare', false, true); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.selectForCompare', + title: { value: localize('localHistory.selectForCompare', "Select for Compare"), original: 'Select for Compare' }, + menu: { + id: MenuId.TimelineItemContext, + group: '2_compare_with', + order: 2, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const contextKeyService = accessor.get(IContextKeyService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + itemSelectedForCompare = item; + LocalHistoryItemSelectedForCompare.bindTo(contextKeyService).set(true); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.compareWithSelected', + title: { value: localize('localHistory.compareWithSelected', "Compare with Selected"), original: 'Compare with Selected' }, + menu: { + id: MenuId.TimelineItemContext, + group: '2_compare_with', + order: 1, + when: ContextKeyExpr.and(LOCAL_HISTORY_MENU_CONTEXT_KEY, LocalHistoryItemSelectedForCompare) + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const commandService = accessor.get(ICommandService); + + if (!itemSelectedForCompare) { + return; + } + + const selectedEntry = (await findLocalHistoryEntry(workingCopyHistoryService, itemSelectedForCompare)).entry; + if (!selectedEntry) { + return; + } + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + return commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(selectedEntry, entry)); + } + } +}); + +//#endregion + +//#region Show Contents + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.open', + title: { value: localize('localHistory.open', "Show Contents"), original: 'Show Contents' }, + menu: { + id: MenuId.TimelineItemContext, + group: '3_contents', + order: 1, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const editorService = accessor.get(IEditorService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + return openEntry(entry, editorService); + } + } +}); + +//#region Restore Contents + +const RESTORE_CONTENTS_LABEL = { value: localize('localHistory.restore', "Restore Contents"), original: 'Restore Contents' }; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.restoreViaEditor', + title: RESTORE_CONTENTS_LABEL, + menu: { + id: MenuId.EditorTitle, + group: 'navigation', + order: -10, + when: ResourceContextKey.Scheme.isEqualTo(LocalHistoryFileSystemProvider.SCHEMA) + }, + icon: LOCAL_HISTORY_ICON_RESTORE + }); + } + async run(accessor: ServicesAccessor, uri: URI): Promise { + const { associatedResource, location } = LocalHistoryFileSystemProvider.fromLocalHistoryFileSystem(uri); + + return restore(accessor, { uri: associatedResource, handle: basenameOrAuthority(location) }); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.restore', + title: RESTORE_CONTENTS_LABEL, + menu: { + id: MenuId.TimelineItemContext, + group: '3_contents', + order: 2, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + return restore(accessor, item); + } +}); + +const restoreSaveSource = SaveSourceRegistry.registerSource('localHistoryRestore.source', localize('localHistoryRestore.source', "File Restored")); + +async function restore(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const fileService = accessor.get(IFileService); + const dialogService = accessor.get(IDialogService); + const workingCopyService = accessor.get(IWorkingCopyService); + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const editorService = accessor.get(IEditorService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmRestoreMessage', "Do you want to restore the contents of '{0}'?", basename(entry.workingCopy.resource)), + detail: localize('confirmRestoreDetail', "Restoring will discard any unsaved changes."), + primaryButton: localize({ key: 'restoreButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Restore"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Revert all dirty working copies for target + const workingCopies = workingCopyService.getAll(entry.workingCopy.resource); + if (workingCopies) { + for (const workingCopy of workingCopies) { + if (workingCopy.isDirty()) { + await workingCopy.revert({ soft: true }); + } + } + } + + // Replace target with contents of history entry + await fileService.cloneFile(entry.location, entry.workingCopy.resource); + + // Restore all working copies for target + if (workingCopies) { + for (const workingCopy of workingCopies) { + await workingCopy.revert({ force: true }); + } + } + + // Open target + await editorService.openEditor({ resource: entry.workingCopy.resource }); + + // Add new entry + await workingCopyHistoryService.addEntry({ + resource: entry.workingCopy.resource, + source: restoreSaveSource + }, CancellationToken.None); + + // Close source + await closeEntry(entry, editorService); + } +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.restoreViaPicker', + title: { value: localize('localHistory.restoreViaPicker', "Find Entry to Restore"), original: 'Find Entry to Restore' }, + f1: true, + category: LOCAL_HISTORY_CATEGORY + }); + } + async run(accessor: ServicesAccessor): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const quickInputService = accessor.get(IQuickInputService); + const modelService = accessor.get(IModelService); + const languageService = accessor.get(ILanguageService); + const labelService = accessor.get(ILabelService); + const editorService = accessor.get(IEditorService); + + // Show all resources with associated history entries in picker + // with progress because this operation will take longer the more + // files have been saved overall. + + const resourcePicker = quickInputService.createQuickPick(); + + let cts = new CancellationTokenSource(); + resourcePicker.onDidHide(() => cts.dispose(true)); + + resourcePicker.busy = true; + resourcePicker.show(); + + const resources = await workingCopyHistoryService.getAll(cts.token); + + resourcePicker.busy = false; + resourcePicker.placeholder = localize('restoreViaPicker.filePlaceholder', "Select the file to show local history for"); + resourcePicker.matchOnLabel = true; + resourcePicker.matchOnDescription = true; + resourcePicker.items = resources.map(resource => ({ + resource, + label: basenameOrAuthority(resource), + description: labelService.getUriLabel(dirname(resource), { relative: true }), + iconClasses: getIconClasses(modelService, languageService, resource) + })).sort((r1, r2) => r1.resource.fsPath < r2.resource.fsPath ? -1 : 1); + + await Event.toPromise(resourcePicker.onDidAccept); + resourcePicker.dispose(); + + const resource = firstOrDefault(resourcePicker.selectedItems)?.resource; + if (!resource) { + return; + } + + // Show all entries for the picked resource in another picker + // and open the entry in the end that was selected by the user + + const entryPicker = quickInputService.createQuickPick(); + + cts = new CancellationTokenSource(); + entryPicker.onDidHide(() => cts.dispose(true)); + + entryPicker.busy = true; + entryPicker.show(); + + const entries = await workingCopyHistoryService.getEntries(resource, cts.token); + + entryPicker.busy = false; + entryPicker.placeholder = localize('restoreViaPicker.entryPlaceholder', "Select the local history entry to open"); + entryPicker.matchOnLabel = true; + entryPicker.matchOnDescription = true; + entryPicker.items = Array.from(entries).reverse().map(entry => ({ + entry, + label: `$(circle-outline) ${SaveSourceRegistry.getSourceLabel(entry.source)}`, + description: toLocalHistoryEntryDateLabel(entry.timestamp) + })); + + await Event.toPromise(entryPicker.onDidAccept); + entryPicker.dispose(); + + const entry = firstOrDefault(entryPicker.selectedItems); + if (!entry) { + return; + } + + return openEntry(entry.entry, editorService); + } +}); + +//#endregion + +//#region Rename + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.rename', + title: { value: localize('localHistory.rename', "Rename"), original: 'Rename' }, + menu: { + id: MenuId.TimelineItemContext, + group: '5_edit', + order: 1, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const quickInputService = accessor.get(IQuickInputService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + const inputBox = quickInputService.createInputBox(); + inputBox.title = localize('renameLocalHistoryEntryTitle', "Rename Local History Entry"); + inputBox.ignoreFocusOut = true; + inputBox.placeholder = localize('renameLocalHistoryPlaceholder', "Enter the new name of the local history entry"); + inputBox.value = SaveSourceRegistry.getSourceLabel(entry.source); + inputBox.show(); + inputBox.onDidAccept(() => { + if (inputBox.value) { + workingCopyHistoryService.updateEntry(entry, { source: inputBox.value }, CancellationToken.None); + } + inputBox.dispose(); + }); + } + } +}); + +//#endregion + +//#region Delete + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.delete', + title: { value: localize('localHistory.delete', "Delete"), original: 'Delete' }, + menu: { + id: MenuId.TimelineItemContext, + group: '5_edit', + order: 2, + when: LOCAL_HISTORY_MENU_CONTEXT_KEY + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const editorService = accessor.get(IEditorService); + const dialogService = accessor.get(IDialogService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmDeleteMessage', "Do you want to delete the local history entry of '{0}' from {1}?", entry.workingCopy.name, toLocalHistoryEntryDateLabel(entry.timestamp)), + detail: localize('confirmDeleteDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'deleteButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Remove via service + await workingCopyHistoryService.removeEntry(entry, CancellationToken.None); + + // Close any opened editors + await closeEntry(entry, editorService); + } + } +}); + +//#endregion + +//#region Delete All + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.deleteAll', + title: { value: localize('localHistory.deleteAll', "Delete All"), original: 'Delete All' }, + f1: true, + category: LOCAL_HISTORY_CATEGORY + }); + } + async run(accessor: ServicesAccessor): Promise { + const dialogService = accessor.get(IDialogService); + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmDeleteAllMessage', "Do you want to delete all entries of all files in local history?"), + detail: localize('confirmDeleteAllDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'deleteAllButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Delete All"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Remove via service + await workingCopyHistoryService.removeAll(CancellationToken.None); + } +}); + +//#endregion + +//#region Helpers + +async function openEntry(entry: IWorkingCopyHistoryEntry, editorService: IEditorService): Promise { + const resource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: entry.location, associatedResource: entry.workingCopy.resource }); + + await editorService.openEditor({ + resource, + label: localize('localHistoryEditorLabel', "{0} ({1} • {2})", entry.workingCopy.name, SaveSourceRegistry.getSourceLabel(entry.source), toLocalHistoryEntryDateLabel(entry.timestamp)) + }); +} + +async function closeEntry(entry: IWorkingCopyHistoryEntry, editorService: IEditorService): Promise { + const resource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: entry.location, associatedResource: entry.workingCopy.resource }); + + const editors = editorService.findEditors(resource, { supportSideBySide: SideBySideEditor.ANY }); + await editorService.closeEditors(editors, { preserveFocus: true }); +} + +export function toDiffEditorArguments(entry: IWorkingCopyHistoryEntry, resource: URI): unknown[]; +export function toDiffEditorArguments(previousEntry: IWorkingCopyHistoryEntry, entry: IWorkingCopyHistoryEntry): unknown[]; +export function toDiffEditorArguments(arg1: IWorkingCopyHistoryEntry, arg2: IWorkingCopyHistoryEntry | URI): unknown[] { + + // Left hand side is always a working copy history entry + const originalResource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: arg1.location, associatedResource: arg1.workingCopy.resource }); + + let label: string; + + // Right hand side depends on how the method was called + // and is either another working copy history entry + // or the file on disk. + + let modifiedResource: URI; + + // Compare with file on disk + if (URI.isUri(arg2)) { + const resource = arg2; + + modifiedResource = resource; + label = localize('localHistoryCompareToFileEditorLabel', "{0} ({1} • {2}) ↔ {3}", arg1.workingCopy.name, SaveSourceRegistry.getSourceLabel(arg1.source), toLocalHistoryEntryDateLabel(arg1.timestamp), arg1.workingCopy.name); + } + + // Compare with another entry + else { + const modified = arg2; + + modifiedResource = LocalHistoryFileSystemProvider.toLocalHistoryFileSystem({ location: modified.location, associatedResource: modified.workingCopy.resource }); + label = localize('localHistoryCompareToPreviousEditorLabel', "{0} ({1} • {2}) ↔ {3} ({4} • {5})", arg1.workingCopy.name, SaveSourceRegistry.getSourceLabel(arg1.source), toLocalHistoryEntryDateLabel(arg1.timestamp), modified.workingCopy.name, SaveSourceRegistry.getSourceLabel(modified.source), toLocalHistoryEntryDateLabel(modified.timestamp)); + } + + return [ + originalResource, + modifiedResource, + label, + undefined // important to keep order of arguments in command proper + ]; +} + +export async function findLocalHistoryEntry(workingCopyHistoryService: IWorkingCopyHistoryService, descriptor: ITimelineCommandArgument): Promise<{ entry: IWorkingCopyHistoryEntry | undefined; previous: IWorkingCopyHistoryEntry | undefined }> { + const entries = await workingCopyHistoryService.getEntries(descriptor.uri, CancellationToken.None); + + let currentEntry: IWorkingCopyHistoryEntry | undefined = undefined; + let previousEntry: IWorkingCopyHistoryEntry | undefined = undefined; + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + + if (entry.id === descriptor.handle) { + currentEntry = entry; + previousEntry = entries[i - 1]; + break; + } + } + + return { + entry: currentEntry, + previous: previousEntry + }; +} + +const SEP = /\//g; +function toLocalHistoryEntryDateLabel(timestamp: number): string { + return `${LOCAL_HISTORY_DATE_FORMATTER.format(timestamp).replace(SEP, '-')}`; // preserving `/` will break editor labels, so replace it with a non-path symbol +} + +//#endregion diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryFileSystemProvider.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryFileSystemProvider.ts new file mode 100644 index 00000000000..8450b8c75f7 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryFileSystemProvider.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { FileDeleteOptions, FileOverwriteOptions, FileSystemProviderCapabilities, FileType, FileWriteOptions, hasReadWriteCapability, IFileService, IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability, IStat, IWatchOptions } from 'vs/platform/files/common/files'; +import { isEqual } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; + +interface ILocalHistoryResource { + + /** + * The location of the local history entry to read from. + */ + readonly location: URI; + + /** + * The associated resource the local history entry is about. + */ + readonly associatedResource: URI; +} + +interface ISerializedLocalHistoryResource { + readonly location: string; + readonly associatedResource: string; +} + +/** + * A wrapper around a standard file system provider + * that is entirely readonly. + */ +export class LocalHistoryFileSystemProvider implements IFileSystemProvider, IFileSystemProviderWithFileReadWriteCapability { + + static readonly SCHEMA = 'vscode-local-history'; + + static toLocalHistoryFileSystem(resource: ILocalHistoryResource): URI { + const serializedLocalHistoryResource: ISerializedLocalHistoryResource = { + location: resource.location.toString(true), + associatedResource: resource.associatedResource.toString(true) + }; + + // Try to preserve the associated resource as much as possible + // and only keep the `query` part dynamic. This enables other + // components (e.g. other timeline providers) to continue + // providing timeline entries even when our resource is active. + return resource.associatedResource.with({ + scheme: LocalHistoryFileSystemProvider.SCHEMA, + query: JSON.stringify(serializedLocalHistoryResource) + }); + } + + static fromLocalHistoryFileSystem(resource: URI): ILocalHistoryResource { + const serializedLocalHistoryResource: ISerializedLocalHistoryResource = JSON.parse(resource.query); + + return { + location: URI.parse(serializedLocalHistoryResource.location), + associatedResource: URI.parse(serializedLocalHistoryResource.associatedResource) + }; + } + + private static readonly EMPTY_RESOURCE = URI.from({ scheme: LocalHistoryFileSystemProvider.SCHEMA, path: '/empty' }); + + static readonly EMPTY: ILocalHistoryResource = { + location: LocalHistoryFileSystemProvider.EMPTY_RESOURCE, + associatedResource: LocalHistoryFileSystemProvider.EMPTY_RESOURCE + }; + + get capabilities() { + return FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.Readonly; + } + + constructor(private readonly fileService: IFileService) { } + + private readonly mapSchemeToProvider = new Map>(); + + private async withProvider(resource: URI): Promise { + const scheme = resource.scheme; + + let providerPromise = this.mapSchemeToProvider.get(scheme); + if (!providerPromise) { + + // Resolve early when provider already exists + const provider = this.fileService.getProvider(scheme); + if (provider) { + providerPromise = Promise.resolve(provider); + } + + // Otherwise wait for registration + else { + providerPromise = new Promise(resolve => { + const disposable = this.fileService.onDidChangeFileSystemProviderRegistrations(e => { + if (e.added && e.provider && e.scheme === scheme) { + disposable.dispose(); + + resolve(e.provider); + } + }); + }); + } + + this.mapSchemeToProvider.set(scheme, providerPromise); + } + + return providerPromise; + } + + //#region Supported File Operations + + async stat(resource: URI): Promise { + const location = LocalHistoryFileSystemProvider.fromLocalHistoryFileSystem(resource).location; + + // Special case: empty resource + if (isEqual(LocalHistoryFileSystemProvider.EMPTY_RESOURCE, location)) { + return { type: FileType.File, ctime: 0, mtime: 0, size: 0 }; + } + + // Otherwise delegate to provider + return (await this.withProvider(location)).stat(location); + } + + async readFile(resource: URI): Promise { + const location = LocalHistoryFileSystemProvider.fromLocalHistoryFileSystem(resource).location; + + // Special case: empty resource + if (isEqual(LocalHistoryFileSystemProvider.EMPTY_RESOURCE, location)) { + return VSBuffer.fromString('').buffer; + } + + // Otherwise delegate to provider + const provider = await this.withProvider(location); + if (hasReadWriteCapability(provider)) { + return provider.readFile(location); + } + + throw new Error('Unsupported'); + } + + //#endregion + + //#region Unsupported File Operations + + readonly onDidChangeCapabilities = Event.None; + readonly onDidChangeFile = Event.None; + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { } + + async mkdir(resource: URI): Promise { } + async readdir(resource: URI): Promise<[string, FileType][]> { return []; } + + async rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { } + async delete(resource: URI, opts: FileDeleteOptions): Promise { } + + watch(resource: URI, opts: IWatchOptions): IDisposable { return Disposable.None; } + + //#endregion +} diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts new file mode 100644 index 00000000000..2ce956d8c86 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { InternalTimelineOptions, ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { URI } from 'vs/base/common/uri'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { API_OPEN_DIFF_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IFileService } from 'vs/platform/files/common/files'; +import { LocalHistoryFileSystemProvider } from 'vs/workbench/contrib/localHistory/browser/localHistoryFileSystemProvider'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { SaveSourceRegistry } from 'vs/workbench/common/editor'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { COMPARE_WITH_FILE_LABEL, toDiffEditorArguments } from 'vs/workbench/contrib/localHistory/browser/localHistoryCommands'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { LOCAL_HISTORY_DATE_FORMATTER, LOCAL_HISTORY_ICON_ENTRY, LOCAL_HISTORY_MENU_CONTEXT_VALUE } from 'vs/workbench/contrib/localHistory/browser/localHistory'; +import { Schemas } from 'vs/base/common/network'; + +export class LocalHistoryTimeline extends Disposable implements IWorkbenchContribution, TimelineProvider { + + private static readonly ID = 'timeline.localHistory'; + + private static readonly LOCAL_HISTORY_ENABLED_SETTINGS_KEY = 'workbench.localHistory.enabled'; + + readonly id = LocalHistoryTimeline.ID; + + readonly label = localize('localHistory', "Local History"); + + readonly scheme = '*'; // we try to show local history for all schemes if possible + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private readonly timelineProviderDisposable = this._register(new MutableDisposable()); + + constructor( + @ITimelineService private readonly timelineService: ITimelineService, + @IWorkingCopyHistoryService private readonly workingCopyHistoryService: IWorkingCopyHistoryService, + @IPathService private readonly pathService: IPathService, + @IFileService private readonly fileService: IFileService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + + this.registerComponents(); + this.registerListeners(); + } + + private registerComponents(): void { + + // Timeline (if enabled) + this.updateTimelineRegistration(); + + // File Service Provider + this._register(this.fileService.registerProvider(LocalHistoryFileSystemProvider.SCHEMA, new LocalHistoryFileSystemProvider(this.fileService))); + } + + private updateTimelineRegistration(): void { + if (this.configurationService.getValue(LocalHistoryTimeline.LOCAL_HISTORY_ENABLED_SETTINGS_KEY)) { + this.timelineProviderDisposable.value = this.timelineService.registerTimelineProvider(this); + } else { + this.timelineProviderDisposable.clear(); + } + } + + private registerListeners(): void { + + // History changes + this._register(this.workingCopyHistoryService.onDidAddEntry(e => this.onDidChangeWorkingCopyHistoryEntry(e.entry, false /* entry added */))); + this._register(this.workingCopyHistoryService.onDidChangeEntry(e => this.onDidChangeWorkingCopyHistoryEntry(e.entry, false /* entry changed */))); + this._register(this.workingCopyHistoryService.onDidReplaceEntry(e => this.onDidChangeWorkingCopyHistoryEntry(e.entry, true /* entry replaced */))); + this._register(this.workingCopyHistoryService.onDidRemoveEntry(e => this.onDidChangeWorkingCopyHistoryEntry(e.entry, true /* entry removed */))); + this._register(this.workingCopyHistoryService.onDidRemoveEntries(() => this.onDidChangeWorkingCopyHistoryEntry(undefined /* all entries */, true /* entry removed */))); + this._register(this.workingCopyHistoryService.onDidMoveEntries(() => this.onDidChangeWorkingCopyHistoryEntry(undefined /* all entries */, true /* entry moved */))); + + // Configuration changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(LocalHistoryTimeline.LOCAL_HISTORY_ENABLED_SETTINGS_KEY)) { + this.updateTimelineRegistration(); + } + })); + } + + private onDidChangeWorkingCopyHistoryEntry(entry: IWorkingCopyHistoryEntry | undefined, reset: boolean): void { + + // Re-emit as timeline change event + this._onDidChange.fire({ + id: LocalHistoryTimeline.ID, + uri: entry?.workingCopy.resource, + reset + }); + } + + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise { + const items: TimelineItem[] = []; + + // Try to convert the provided `uri` into a form that is likely + // for the provider to find entries for: + // - `vscode-local-history`: convert back to the associated resource + // - default-scheme / settings: keep as is + // - anything that is backed by a file system provider: convert + let resource: URI | undefined = undefined; + if (uri.scheme === LocalHistoryFileSystemProvider.SCHEMA) { + resource = LocalHistoryFileSystemProvider.fromLocalHistoryFileSystem(uri).associatedResource; + } else if (uri.scheme === this.pathService.defaultUriScheme || uri.scheme === Schemas.vscodeUserData) { + resource = uri; + } else if (this.fileService.hasProvider(uri)) { + resource = URI.from({ scheme: this.pathService.defaultUriScheme, authority: this.environmentService.remoteAuthority, path: uri.path }); + } + + if (resource) { + + // Retrieve from working copy history + const entries = await this.workingCopyHistoryService.getEntries(resource, token); + + // Convert to timeline items + for (const entry of entries) { + items.push(this.toTimelineItem(entry)); + } + } + + return { + source: LocalHistoryTimeline.ID, + items + }; + } + + private toTimelineItem(entry: IWorkingCopyHistoryEntry): TimelineItem { + return { + handle: entry.id, + label: SaveSourceRegistry.getSourceLabel(entry.source), + tooltip: new MarkdownString(`$(history) ${LOCAL_HISTORY_DATE_FORMATTER.format(entry.timestamp)}\n\n${SaveSourceRegistry.getSourceLabel(entry.source)}`, { supportThemeIcons: true }), + source: LocalHistoryTimeline.ID, + timestamp: entry.timestamp, + themeIcon: LOCAL_HISTORY_ICON_ENTRY, + contextValue: LOCAL_HISTORY_MENU_CONTEXT_VALUE, + command: { + id: API_OPEN_DIFF_EDITOR_COMMAND_ID, + title: COMPARE_WITH_FILE_LABEL.value, + arguments: toDiffEditorArguments(entry, entry.workingCopy.resource) + } + }; + } +} diff --git a/extensions/markdown-language-features/src/util/path.ts b/src/vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribution.ts similarity index 76% rename from extensions/markdown-language-features/src/util/path.ts rename to src/vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribution.ts index f25fd080975..0aa8970b30a 100644 --- a/extensions/markdown-language-features/src/util/path.ts +++ b/src/vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribution.ts @@ -3,6 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/// - -export { basename, dirname, extname, isAbsolute, join } from 'path'; +import 'vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands'; diff --git a/src/vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands.ts b/src/vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands.ts new file mode 100644 index 00000000000..0acd639b851 --- /dev/null +++ b/src/vs/workbench/contrib/localHistory/electron-sandbox/localHistoryCommands.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { LOCAL_HISTORY_MENU_CONTEXT_KEY } from 'vs/workbench/contrib/localHistory/browser/localHistory'; +import { findLocalHistoryEntry, ITimelineCommandArgument } from 'vs/workbench/contrib/localHistory/browser/localHistoryCommands'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Schemas } from 'vs/base/common/network'; +import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; + +//#region Delete + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.localHistory.revealInOS', + title: { + value: isWindows ? localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? localize('revealInMac', "Reveal in Finder") : localize('openContainer', "Open Containing Folder"), + original: isWindows ? 'Reveal in File Explorer' : isMacintosh ? 'Reveal in Finder' : 'Open Containing Folder' + }, + menu: { + id: MenuId.TimelineItemContext, + group: '4_reveal', + order: 1, + when: ContextKeyExpr.and(LOCAL_HISTORY_MENU_CONTEXT_KEY, ResourceContextKey.Scheme.isEqualTo(Schemas.file)) + } + }); + } + async run(accessor: ServicesAccessor, item: ITimelineCommandArgument): Promise { + const workingCopyHistoryService = accessor.get(IWorkingCopyHistoryService); + const nativeHostService = accessor.get(INativeHostService); + + const { entry } = await findLocalHistoryEntry(workingCopyHistoryService, item); + if (entry) { + await nativeHostService.showItemInFolder(entry.location.fsPath); + } + } +}); + +//#endregion diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index 01b29091f33..c47ff0b58ff 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { hookDomPurifyHrefAndSrcSanitizer } from 'vs/base/browser/dom'; import * as dompurify from 'vs/base/browser/dompurify/dompurify'; import { marked } from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; -import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { tokenizeToString } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export const DEFAULT_MARKDOWN_STYLES = ` @@ -152,21 +153,7 @@ code > div { const allowedProtocols = [Schemas.http, Schemas.https, Schemas.command]; function sanitize(documentContent: string, allowUnknownProtocols: boolean): string { - // https://github.com/cure53/DOMPurify/blob/main/demos/hooks-scheme-allowlist.html - dompurify.addHook('afterSanitizeAttributes', (node) => { - // build an anchor to map URLs to - const anchor = document.createElement('a'); - - // check all href/src attributes for validity - for (const attr of ['href', 'src']) { - if (node.hasAttribute(attr)) { - anchor.href = node.getAttribute(attr) as string; - if (!allowedProtocols.includes(anchor.protocol)) { - node.removeAttribute(attr); - } - } - } - }); + const hook = hookDomPurifyHrefAndSrcSanitizer(allowedProtocols, true); try { return dompurify.sanitize(documentContent, { @@ -186,7 +173,7 @@ function sanitize(documentContent: string, allowUnknownProtocols: boolean): stri ...(allowUnknownProtocols ? { ALLOW_UNKNOWN_PROTOCOLS: true } : {}), }); } finally { - dompurify.removeHook('afterSanitizeAttributes'); + hook.dispose(); } } diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index f9f72561b0f..b17ef050f94 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -328,6 +328,7 @@ registerAction2(class extends ViewAction { keybinding: { when: Constants.MarkerViewFilterFocusContextKey, weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape }, viewId: Constants.MARKERS_VIEW_ID }); diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 3da2df008f5..63dc3458879 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -411,7 +411,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { if (event.equals(KeyCode.Space) || event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow) - || event.equals(KeyCode.Escape) ) { event.stopPropagation(); } @@ -419,10 +418,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onInputKeyDown(event: StandardKeyboardEvent, filterInputBox: HistoryInputBox) { let handled = false; - if (event.equals(KeyCode.Escape)) { - this.clearFilterText(); - handled = true; - } if (event.equals(KeyCode.Tab)) { this.actionbar?.focus(); handled = true; diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index d6dbfb2c18f..37714bd60e8 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -103,7 +103,8 @@ padding-right: 10px; } -.monaco-workbench.hc-black .markers-panel .markers-panel-container .tree-container .monaco-tl-contents { +.monaco-workbench.hc-black .markers-panel .markers-panel-container .tree-container .monaco-tl-contents, +.monaco-workbench.hc-light .markers-panel .markers-panel-container .tree-container .monaco-tl-contents { line-height: 20px; } diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 3e615d39368..e5d0ab10ba4 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -49,7 +49,7 @@ export default class Messages { public static MARKERS_PANEL_SINGLE_UNKNOWN_LABEL: string = nls.localize('markers.panel.single.unknown.label', "1 Unknown"); public static readonly MARKERS_PANEL_MULTIPLE_UNKNOWNS_LABEL = (noOfUnknowns: number): string => { return nls.localize('markers.panel.multiple.unknowns.label', "{0} Unknowns", '' + noOfUnknowns); }; - public static readonly MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "[{0}, {1}]", '' + ln, '' + col); }; + public static readonly MARKERS_PANEL_AT_LINE_COL_NUMBER = (ln: number, col: number): string => { return nls.localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + ln, '' + col); }; public static readonly MARKERS_TREE_ARIA_LABEL_RESOURCE = (noOfProblems: number, fileName: string, folder: string): string => { return nls.localize('problems.tree.aria.label.resource', "{0} problems in file {1} of folder {2}", noOfProblems, fileName, folder); }; public static readonly MARKERS_TREE_ARIA_LABEL_MARKER = (marker: Marker): string => { 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 6eae7cd10c9..2adc303d4e1 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellCommands/cellCommands.ts @@ -335,12 +335,6 @@ registerAction2(class CollapseCellInputAction extends NotebookMultiCellAction { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated()), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), weight: KeybindingWeight.WorkbenchContrib - }, - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated()), - group: CellOverflowToolbarGroups.Collapse, - order: 0 } }); } @@ -367,12 +361,6 @@ registerAction2(class ExpandCellInputAction extends NotebookMultiCellAction { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyC), weight: KeybindingWeight.WorkbenchContrib - }, - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_INPUT_COLLAPSED), - group: CellOverflowToolbarGroups.Collapse, - order: 1 } }); } @@ -399,12 +387,6 @@ registerAction2(class CollapseCellOutputAction extends NotebookMultiCellAction { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyT), weight: KeybindingWeight.WorkbenchContrib - }, - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), - group: CellOverflowToolbarGroups.Collapse, - order: 2 } }); } @@ -427,12 +409,6 @@ registerAction2(class ExpandCellOuputAction extends NotebookMultiCellAction { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyT), weight: KeybindingWeight.WorkbenchContrib - }, - menu: { - id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_CELL_OUTPUT_COLLAPSED), - group: CellOverflowToolbarGroups.Collapse, - order: 3 } }); } 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 82fdd9396e0..47d41741736 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController.ts @@ -170,7 +170,7 @@ class ExecutionStateCellStatusBarItem extends Disposable { priority: Number.MAX_SAFE_INTEGER }; } else if (state === NotebookCellExecutionState.Executing) { - const icon = runState?.isPaused ? + const icon = runState?.didPause ? executingStateIcon : ThemeIcon.modify(executingStateIcon, 'spin'); return { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/execute/execution.ts b/src/vs/workbench/contrib/notebook/browser/contrib/execute/execution.ts deleted file mode 100644 index 0572d8ae0e2..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/execute/execution.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; -import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; - -export class ExecutionContrib extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.executionContrib'; - - constructor( - private readonly _notebookEditor: INotebookEditor, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, - @INotebookExecutionService private readonly _notebookExecutionService: INotebookExecutionService, - @ILogService private readonly _logService: ILogService, - @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, - ) { - super(); - - this._register(this._notebookKernelService.onDidChangeSelectedNotebooks(e => { - if (e.newKernel && this._notebookEditor.textModel?.uri.toString() === e.notebook.toString()) { - this.cancelAll(); - this._notebookExecutionStateService.forceCancelNotebookExecutions(e.notebook); - } - })); - } - - private cancelAll(): void { - this._logService.debug(`ExecutionContrib#cancelAll`); - const exes = this._notebookExecutionStateService.getCellExecutionStatesForNotebook(this._notebookEditor.textModel!.uri); - this._notebookExecutionService.cancelNotebookCellHandles(this._notebookEditor.textModel!, exes.map(exe => exe.cellHandle)); - } -} - - -registerNotebookContribution(ExecutionContrib.id, ExecutionContrib); 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 4a56e7d6acf..2aef79dc87b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress.ts @@ -19,7 +19,7 @@ export class ExecutionEditorProgressController extends Disposable implements INo ) { super(); - this._register(_notebookEditor.onDidChangeVisibleRanges(() => this._update())); + this._register(_notebookEditor.onDidScroll(() => this._update())); this._register(_notebookExecutionStateService.onDidChangeCellExecution(e => { if (e.notebook.toString() !== this._notebookEditor.textModel?.uri.toString()) { @@ -44,7 +44,10 @@ export class ExecutionEditorProgressController extends Disposable implements INo for (const range of this._notebookEditor.visibleRanges) { for (const cell of this._notebookEditor.getCellsInRange(range)) { if (cell.handle === exe.cellHandle) { - return true; + const top = this._notebookEditor.getAbsoluteTopOfElement(cell); + if (this._notebookEditor.scrollTop < top + 30) { + 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 06ff1c4900d..0b607f7d085 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; -import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex, OutputFindMatch, ICellModelDecorations, ICellModelDeltaDecorations } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex, OutputFindMatch, ICellModelDecorations, ICellModelDeltaDecorations, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; import { FindDecorations } from 'vs/editor/contrib/find/browser/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -18,6 +18,7 @@ import { findFirstInSorted } from 'vs/base/common/arrays'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; +import { overviewRulerFindMatchForeground, overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; export class FindModel extends Disposable { @@ -25,6 +26,8 @@ export class FindModel extends Disposable { protected _findMatchesStarts: PrefixSumComputer | null = null; private _currentMatch: number = -1; private _allMatchesDecorations: ICellModelDecorations[] = []; + private _currentMatchCellDecorations: string[] = []; + private _allMatchesCellDecorations: string[] = []; private _currentMatchDecorations: { kind: 'input'; decorations: ICellModelDecorations[] } | { kind: 'output'; index: number } | null = null; private readonly _throttledDelayer: Delayer; private _computePromise: CancelablePromise | null = null; @@ -391,6 +394,18 @@ export class FindModel extends Disposable { }; }); + this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ + ownerId: cell.handle, + handle: cell.handle, + options: { + overviewRuler: { + color: overviewRulerSelectionHighlightForeground, + modelRanges: [match.range], + includeOutput: false + } + } + } as INotebookDeltaDecoration]); + return null; } else { this.clearCurrentFindMatchDecoration(); @@ -398,6 +413,19 @@ export class FindModel extends Disposable { const match = this._findMatches[cellIndex].matches[matchIndex] as OutputFindMatch; const offset = await this._notebookEditor.highlightFind(cell, match.index); this._currentMatchDecorations = { kind: 'output', index: match.index }; + + this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ + ownerId: cell.handle, + handle: cell.handle, + options: { + overviewRuler: { + color: overviewRulerSelectionHighlightForeground, + modelRanges: [], + includeOutput: true + } + } + } as INotebookDeltaDecoration]); + return offset; } } @@ -411,6 +439,8 @@ export class FindModel extends Disposable { } else if (this._currentMatchDecorations?.kind === 'output') { this._notebookEditor.unHighlightFind(this._currentMatchDecorations.index); } + + this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, []); } private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { @@ -435,6 +465,20 @@ export class FindModel extends Disposable { this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); }); + + this._allMatchesCellDecorations = this._notebookEditor.deltaCellDecorations(this._allMatchesCellDecorations, cellFindMatches.map(cellFindMatch => { + return { + ownerId: cellFindMatch.cell.handle, + handle: cellFindMatch.cell.handle, + options: { + overviewRuler: { + color: overviewRulerFindMatchForeground, + modelRanges: cellFindMatch.matches.slice(0, cellFindMatch.modelMatchCount).map(match => (match as FindMatch).range), + includeOutput: cellFindMatch.modelMatchCount < cellFindMatch.matches.length + } + } + }; + })); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css index 8b380c1d39c..d9701c95197 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.css @@ -16,6 +16,14 @@ visibility: hidden; } +.monaco-workbench.reduce-motion .simple-fr-find-part-wrapper { + transition: top 0ms linear; +} + +.monaco-workbench .notebookOverlay .simple-fr-find-part-wrapper.visible { + z-index: 100; +} + .monaco-workbench .simple-fr-find-part { /* visibility: hidden; Use visibility to maintain flex layout while hidden otherwise interferes with transition */ z-index: 10; @@ -119,7 +127,7 @@ cursor: default; } -.monaco-workbench .simple-fr-find-part-wrapper .monaco-custom-checkbox.disabled { +.monaco-workbench .simple-fr-find-part-wrapper .monaco-custom-toggle.disabled { opacity: 0.3; cursor: default; user-select: none; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index b66379399c8..239aa8064cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -91,7 +91,7 @@ registerAction2(class extends Action2 { return []; })); - await bulkEditService.apply(/* edit */flatten(allCellEdits), { label: localize('label', "Format Notebook") }); + await bulkEditService.apply(/* edit */flatten(allCellEdits), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', }); } finally { disposable.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index d0b7933d3f3..1c601fbf36f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -17,7 +17,6 @@ import { IEditorCommandsContext } from 'vs/workbench/common/editor'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; -import { flatten } from 'vs/base/common/arrays'; import { TypeConstraint } from 'vs/base/common/types'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { MarshalledId } from 'vs/base/common/marshallingIds'; @@ -43,8 +42,7 @@ export const enum CellToolbarOrder { export const enum CellOverflowToolbarGroups { Copy = '1_copy', Insert = '2_insert', - Edit = '3_edit', - Collapse = '4_collapse', + Edit = '3_edit' } export interface INotebookActionContext { @@ -335,7 +333,7 @@ export function parseMultiCellExecutionArgs(accessor: ServicesAccessor, ...args: } const ranges = firstArg.ranges; - const selectedCells = flatten(ranges.map(range => editor.getCellsInRange(range).slice(0))); + const selectedCells = ranges.map(range => editor.getCellsInRange(range).slice(0)).flat(); const autoReveal = firstArg.autoReveal; return { ui: false, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index dec1acd7471..602e14a3940 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -147,10 +147,6 @@ registerAction2(class DeleteCellAction extends NotebookCellAction { { id: DELETE_CELL_COMMAND_ID, title: localize('notebookActions.deleteCell', "Delete Cell"), - menu: { - id: MenuId.NotebookCellTitle, - when: NOTEBOOK_EDITOR_EDITABLE - }, keybinding: { primary: KeyCode.Delete, mac: { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 4f58d8bd162..5e007b57a28 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -8,7 +8,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { DiffElementViewModelBase, getFormattedMetadataJSON, getFormattedOutputJSON, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; +import { DiffElementViewModelBase, getFormattedMetadataJSON, getFormattedOutputJSON, OutputComparison, outputEqual, OUTPUT_EDITOR_HEIGHT_MAGIC, PropertyFoldingState, SideBySideDiffElementViewModel, SingleSideDiffElementViewModel } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { CellDiffSideBySideRenderTemplate, CellDiffSingleSideRenderTemplate, DiffSide, DIFF_CELL_MARGIN, INotebookTextDiffEditor, NOTEBOOK_DIFF_CELL_INPUT, NOTEBOOK_DIFF_CELL_PROPERTY, NOTEBOOK_DIFF_CELL_PROPERTY_EXPANDED } from 'vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -286,12 +286,13 @@ abstract class AbstractElementRenderer extends Disposable { protected _outputViewContainer?: HTMLElement; protected _outputLeftContainer?: HTMLElement; protected _outputRightContainer?: HTMLElement; + protected _outputMetadataContainer?: HTMLElement; protected _outputEmptyElement?: HTMLElement; protected _outputLeftView?: OutputContainer; protected _outputRightView?: OutputContainer; protected _outputEditorDisposeStore!: DisposableStore; protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; - + protected _outputMetadataEditor?: DiffEditorWidget; protected _diffEditorContainer!: HTMLElement; protected _diagonalFill?: HTMLElement; @@ -1398,8 +1399,16 @@ export class ModifiedElement extends AbstractElementRenderer { this._outputLeftContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-left')); this._outputRightContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-right')); + this._outputMetadataContainer = DOM.append(this._outputViewContainer!, DOM.$('.output-view-container-metadata')); - if (this.cell.checkIfOutputsModified()) { + const outputModified = this.cell.checkIfOutputsModified(); + const outputMetadataChangeOnly = outputModified + && outputModified.kind === OutputComparison.Metadata + && this.cell.original!.outputs.length === 1 + && this.cell.modified!.outputs.length === 1 + && outputEqual(this.cell.original!.outputs[0], this.cell.modified!.outputs[0]) === OutputComparison.Metadata; + + if (outputModified && !outputMetadataChangeOnly) { const originalOutputRenderListener = this.notebookEditor.onDidDynamicOutputRendered(e => { if (e.cell.uri.toString() === this.cell.original.uri.toString()) { this.notebookEditor.deltaCellOutputContainerClassNames(DiffSide.Original, this.cell.original.id, ['nb-cellDeleted'], []); @@ -1425,7 +1434,48 @@ export class ModifiedElement extends AbstractElementRenderer { this._outputRightView = this.instantiationService.createInstance(OutputContainer, this.notebookEditor, this.notebookEditor.textModel!, this.cell, this.cell.modified!, DiffSide.Modified, this._outputRightContainer!); this._outputRightView.render(); this._register(this._outputRightView); - this._decorate(); + + if (outputModified && !outputMetadataChangeOnly) { + this._decorate(); + } + + if (outputMetadataChangeOnly) { + + this._outputMetadataContainer.style.top = `${this.cell.layoutInfo.rawOutputHeight}px`; + // single output, metadata change, let's render a diff editor for metadata + this._outputMetadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputMetadataContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true, + ignoreTrimWhitespace: false, + automaticLayout: false, + dimension: { + height: OUTPUT_EDITOR_HEIGHT_MAGIC, + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true) + } + }, { + originalEditor: getOptimizedNestedCodeEditorWidgetOptions(), + modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() + }); + this._register(this._outputMetadataEditor); + const originalOutputMetadataSource = JSON.stringify(this.cell.original!.outputs[0].metadata ?? {}, undefined, '\t'); + const modifiedOutputMetadataSource = JSON.stringify(this.cell.modified!.outputs[0].metadata ?? {}, undefined, '\t'); + + const mode = this.languageService.createById('json'); + const originalModel = this.modelService.createModel(originalOutputMetadataSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputMetadataSource, mode, undefined, true); + + this._outputMetadataEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + + this.cell.outputMetadataHeight = this._outputMetadataEditor.getContentHeight(); + + this._register(this._outputMetadataEditor.onDidContentSizeChange((e) => { + this.cell.outputMetadataHeight = e.contentHeight; + })); + } } this._outputViewContainer.style.display = 'block'; @@ -1444,6 +1494,7 @@ export class ModifiedElement extends AbstractElementRenderer { this._outputLeftView?.showOutputs(); this._outputRightView?.showOutputs(); + this._outputMetadataEditor?.layout(); this._decorate(); } } @@ -1572,6 +1623,12 @@ export class ModifiedElement extends AbstractElementRenderer { this._outputEditorContainer.style.height = `${this.cell.layoutInfo.outputTotalHeight}px`; this._outputEditor?.layout(); } + + if (this._outputMetadataContainer) { + this._outputMetadataContainer.style.height = `${this.cell.layoutInfo.outputMetadataHeight}px`; + this._outputMetadataContainer.style.top = `${this.cell.layoutInfo.outputTotalHeight - this.cell.layoutInfo.outputMetadataHeight}px`; + this._outputMetadataEditor?.layout(); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 2c65e2d5ffd..eea2a5a0c8c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -56,6 +56,14 @@ export abstract class DiffElementViewModelBase extends Disposable { throw new Error('Use Cell.layoutInfo.outputStatusHeight'); } + set outputMetadataHeight(height: number) { + this._layout({ outputMetadataHeight: height }); + } + + get outputMetadataHeight() { + throw new Error('Use Cell.layoutInfo.outputStatusHeight'); + } + set editorHeight(height: number) { this._layout({ editorHeight: height }); } @@ -129,6 +137,7 @@ export abstract class DiffElementViewModelBase extends Disposable { rawOutputHeight: 0, outputTotalHeight: 0, outputStatusHeight: 25, + outputMetadataHeight: 0, bodyMargin: 32, totalHeight: 82, layoutState: CellLayoutState.Uninitialized @@ -155,7 +164,8 @@ export abstract class DiffElementViewModelBase extends Disposable { const rawOutputHeight = delta.rawOutputHeight !== undefined ? delta.rawOutputHeight : this._layoutInfo.rawOutputHeight; const outputStatusHeight = delta.outputStatusHeight !== undefined ? delta.outputStatusHeight : this._layoutInfo.outputStatusHeight; const bodyMargin = delta.bodyMargin !== undefined ? delta.bodyMargin : this._layoutInfo.bodyMargin; - const outputHeight = (delta.recomputeOutput || delta.rawOutputHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight) : this._layoutInfo.outputTotalHeight; + const outputMetadataHeight = delta.outputMetadataHeight !== undefined ? delta.outputMetadataHeight : this._layoutInfo.outputMetadataHeight; + const outputHeight = (delta.recomputeOutput || delta.rawOutputHeight !== undefined || delta.outputMetadataHeight !== undefined) ? this._getOutputTotalHeight(rawOutputHeight, outputMetadataHeight) : this._layoutInfo.outputTotalHeight; const totalHeight = editorHeight + editorMargin @@ -175,6 +185,7 @@ export abstract class DiffElementViewModelBase extends Disposable { outputStatusHeight: outputStatusHeight, bodyMargin: bodyMargin, rawOutputHeight: rawOutputHeight, + outputMetadataHeight: outputMetadataHeight, totalHeight: totalHeight, layoutState: CellLayoutState.Measured }; @@ -213,6 +224,10 @@ export abstract class DiffElementViewModelBase extends Disposable { changeEvent.bodyMargin = true; } + if (newLayout.outputMetadataHeight !== this._layoutInfo.outputMetadataHeight) { + changeEvent.outputMetadataHeight = true; + } + if (newLayout.totalHeight !== this._layoutInfo.totalHeight) { changeEvent.totalHeight = true; } @@ -237,6 +252,7 @@ export abstract class DiffElementViewModelBase extends Disposable { + this._layoutInfo.metadataStatusHeight + this._layoutInfo.outputTotalHeight + this._layoutInfo.outputStatusHeight + + this._layoutInfo.outputMetadataHeight + this._layoutInfo.bodyMargin; return totalHeight; @@ -253,7 +269,7 @@ export abstract class DiffElementViewModelBase extends Disposable { + verticalScrollbarHeight; } - private _getOutputTotalHeight(rawOutputHeight: number) { + private _getOutputTotalHeight(rawOutputHeight: number, metadataHeight: number) { if (this.outputFoldingState === PropertyFoldingState.Collapsed) { return 0; } @@ -263,7 +279,7 @@ export abstract class DiffElementViewModelBase extends Disposable { // single line; return 24; } - return this.getRichOutputTotalHeight(); + return this.getRichOutputTotalHeight() + metadataHeight; } else { return rawOutputHeight; } @@ -386,7 +402,8 @@ export class SideBySideDiffElementViewModel extends DiffElementViewModelBase { } return { - reason: ret === OutputComparison.Metadata ? 'Output metadata is changed' : undefined + reason: ret === OutputComparison.Metadata ? 'Output metadata is changed' : undefined, + kind: ret }; } @@ -553,12 +570,40 @@ export class SingleSideDiffElementViewModel extends DiffElementViewModelBase { } } -const enum OutputComparison { +export const enum OutputComparison { Unchanged = 0, Metadata = 1, Other = 2 } +export function outputEqual(a: ICellOutput, b: ICellOutput): OutputComparison { + if (hash(a.metadata) === hash(b.metadata)) { + return OutputComparison.Other; + } + + // metadata not equal + for (let j = 0; j < a.outputs.length; j++) { + const aOutputItem = a.outputs[j]; + const bOutputItem = b.outputs[j]; + + if (aOutputItem.mime !== bOutputItem.mime) { + return OutputComparison.Other; + } + + if (aOutputItem.data.buffer.length !== bOutputItem.data.buffer.length) { + return OutputComparison.Other; + } + + for (let k = 0; k < aOutputItem.data.buffer.length; k++) { + if (aOutputItem.data.buffer[k] !== bOutputItem.data.buffer[k]) { + return OutputComparison.Other; + } + } + } + + return OutputComparison.Metadata; +} + function outputsEqual(original: ICellOutput[], modified: ICellOutput[]) { if (original.length !== modified.length) { return OutputComparison.Other; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css index 5dc1777ddb0..4f2ea74fe77 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -173,11 +173,6 @@ position: relative; } -.monaco-workbench .notebook-text-diff-editor .cell-body .output-view-container .output-plaintext { - white-space: pre; - overflow-x: hidden; -} - .monaco-workbench .notebook-text-diff-editor .cell-body.left .output-view-container .output-inner-container, .monaco-workbench .notebook-text-diff-editor .cell-body.right .output-view-container .output-inner-container { width: 100%; @@ -282,6 +277,10 @@ color: inherit; } +.monaco-workbench .notebook-text-diff-editor .output-view-container .output-view-container-metadata { + position: relative; +} + /* Diff decorations */ .notebook-text-diff-editor .cell-body .codicon-diff-remove, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts index 72b2d4b4ef7..9bb383e2b81 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditorBrowser.ts @@ -98,6 +98,7 @@ export interface IDiffElementLayoutInfo { metadataHeight: number; metadataStatusHeight: number; rawOutputHeight: number; + outputMetadataHeight: number; outputTotalHeight: number; outputStatusHeight: number; bodyMargin: number; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index c4f9152f8ff..83b1319c455 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -379,20 +379,29 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD this._modifiedWebview.dispose(); } - this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; + this._modifiedWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { + ...this._notebookOptions.computeDiffWebviewOptions(), + fontFamily: this._generateFontFamily() + }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._modifiedWebview.element); await this._modifiedWebview.createWebview(); this._modifiedWebview.element.style.width = `calc(50% - 16px)`; this._modifiedWebview.element.style.left = `calc(50%)`; } + _generateFontFamily(): string { + return this._fontInfo?.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; + } private async _createOriginalWebview(id: string, resource: URI): Promise { if (this._originalWebview) { this._originalWebview.dispose(); } - this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, this._notebookOptions.computeDiffWebviewOptions(), undefined) as BackLayerWebView; + this._originalWebview = this.instantiationService.createInstance(BackLayerWebView, this, id, resource, { + ...this._notebookOptions.computeDiffWebviewOptions(), + fontFamily: this._generateFontFamily() + }, undefined) as BackLayerWebView; // attach the webview container to the DOM tree first this._list.rowsContainer.insertAdjacentElement('afterbegin', this._originalWebview.element); await this._originalWebview.createWebview(); @@ -806,7 +815,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD return { width: this._dimension!.width, height: this._dimension!.height, - fontInfo: this._fontInfo! + fontInfo: this._fontInfo!, + scrollHeight: this._list?.getScrollHeight() ?? 0, }; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts index 090b45f6f1d..b774588523b 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -312,6 +312,10 @@ export class NotebookTextDiffList extends WorkbenchList diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index af7e7fbef82..a660892ff1f 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -11,86 +11,8 @@ position: relative; } -.monaco-workbench .notebookOverlay .notebook-toolbar-container { - width: 100%; - display: none; - margin-top: 2px; - margin-bottom: 2px; - contain: style; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item { - height: 22px; - display: flex; - align-items: center; - border-radius: 5px; - margin-right: 8px; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element { - flex: 1; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element .notebook-toolbar-left { - padding: 0px 0px 0px 8px; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { - display: flex; - padding: 0px 0px 0px 0px; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .kernel-label { - background-size: 16px; - padding: 0px 5px 0px 3px; - border-radius: 5px; - font-size: 13px; - height: 22px; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left .monaco-action-bar .action-item .action-label.separator { - margin: 5px 0px !important; - padding: 0px !important; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item:not(.disabled):hover { - background-color: var(--vscode-toolbar-hoverBackground); -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .action-label { - background-size: 16px; - padding-left: 2px; -} - -.monaco-workbench .notebook-action-view-item .action-label { - display: inline-flex; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { - background-size: 16px; - padding: 0px 5px 0px 2px; - border-radius: 5px; - background-color: unset; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item.disabled .notebook-label { - opacity: 0.4; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active .action-label:not(.disabled) { - background-color: unset; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover { - background-color: unset; -} - -.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { - background-color: unset; -} - -.monaco-workbench .notebookOverlay .simple-fr-find-part-wrapper.visible { - z-index: 100; +.monaco-workbench .notebookOverlay .cell-list-container > .monaco-list { + position: absolute; } .monaco-workbench .notebookOverlay .cell-list-container .overflowingContentWidgets > div { @@ -200,13 +122,6 @@ opacity: 0.33; } -.monaco-workbench .notebookOverlay .notebook-content-widgets { - position: absolute; - top: 0; - left: 0; - width: 100%; -} - .monaco-workbench .notebookOverlay .output { position: absolute; height: 0px; @@ -247,22 +162,6 @@ transform: translate3d(0px, 0px, 0px); } -.monaco-workbench .notebookOverlay .output > div.foreground .output-stream, -.monaco-workbench .notebookOverlay .output > div.foreground .output-plaintext { - font-family: var(--notebook-cell-output-font-family); - white-space: pre-wrap; - word-wrap: break-word; -} - -.monaco-workbench .notebookOverlay .output > div.foreground .output-stream pre, -.monaco-workbench .notebookOverlay .output > div.foreground .output-plaintext pre { - font-family: var(--notebook-cell-output-font-family); -} - -.monaco-workbench .notebookOverlay .output > div.foreground.error .output-stream { - color: red; /*TODO@rebornix theme color*/ -} - .monaco-workbench .notebookOverlay .cell-drag-image .output .cell-output-toolbar { display: none; } @@ -455,23 +354,8 @@ opacity: 1; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top:before { - top: 0; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { - left: 0; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { - bottom: 0px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { - right: 0; -} - -.monaco-workbench.hc-black .notebookOverlay .monaco-list-row.focused .cell-editor-focus .cell-editor-part:before { +.monaco-workbench.hc-black .notebookOverlay .monaco-list-row.focused .cell-editor-focus .cell-editor-part:before, +.monaco-workbench.hc-light .notebookOverlay .monaco-list-row.focused .cell-editor-focus .cell-editor-part:before { outline-style: dashed; } @@ -480,108 +364,6 @@ cursor: pointer; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - opacity: 0; - display: inline-flex; - position: absolute; - height: 26px; - top: -14px; - /* this lines up the bottom toolbar border with the current line when on line 01 */ - z-index: var(--z-index-notebook-cell-toolbar); -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { - z-index: var(--z-index-notebook-cell-toolbar-dropdown-active); -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item.menu-entry { - width: 24px; - height: 24px; - display: flex; - align-items: center; - margin: 1px 2px; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .action-label { - display: flex; - align-items: center; - margin: auto; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown { - width: 100%; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown .dropdown-label { - display: flex; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container { - height: 22px; - font-size: 12px; - display: flex; - position: relative; - overflow: hidden; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-hidden .cell-statusbar-container { - display: none; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { - display: flex; - flex-grow: 1; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left, -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { - display: flex; - z-index: var(--z-index-notebook-cell-status); -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items { - justify-content: flex-end; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-contributed-items { - display: flex; - flex-wrap: wrap; - overflow: hidden; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item { - display: flex; - align-items: center; - white-space: pre; - - height: 21px; /* Editor outline is -1px in, don't overlap */ - margin: 0px 3px; - padding: 0px 3px; - overflow: hidden; - text-overflow: clip; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command { - cursor: pointer; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left > .cell-contributed-items { - margin-left: 10px; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .codicon { - font-size: 14px; - color: unset; /* Inherit from parent cell-status-item */ -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item-show-when-active { - display: none; -} - -.monaco-workbench .notebookOverlay .cell-statusbar-container.is-active-cell .cell-status-item-show-when-active { - display: initial; -} - .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { position: absolute; flex-shrink: 0; @@ -643,107 +425,15 @@ height: 2px; } -/* toolbar visible on hover */ -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, -.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { - opacity: 1; -} - -/* toolbar visible on click */ -.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { - visibility: hidden; -} -.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-title-toolbar { - opacity: 1; - visibility: visible; -} - .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator { - position: absolute; - top: 0px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { - /** Overidden for code cells */ - top: 0px; - bottom: 0px; -} - -.monaco-workbench .notebookOverlay .monaco-list .webview-backed-markdown-cell .cell-focus-indicator-side { - /* Disable pointer events for the folding container */ - pointer-events: none; -} - -.monaco-workbench .notebookOverlay .monaco-list .webview-backed-markdown-cell .cell-focus-indicator-side .notebook-folding-indicator { - /* But allow clicking on the folding indicator itself */ - pointer-events: all; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { - width: 100%; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right { - right: 0px; -} - -/** cell border colors */ - -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { - border-color: var(notebook-selected-cell-border-color) !important; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before { - border-color: var(--notebook-inactive-focused-cell-border-color) !important; -} - -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-top:before, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-bottom:before, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { - border-color: var(--notebook-focused-cell-border-color) !important; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator-container { - display: none; - position: relative; - cursor: pointer; - pointer-events: all; /* Take pointer-events in markdown cell */ - width: 11px; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator { - width: 0px; - height: 100%; -} - -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left, -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-inner-container { - cursor: grab; -} - .monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { z-index: var(--z-index-notebook-scrollbar); cursor: default; } -.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator .codicon:hover { - cursor: pointer; -} - .monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { z-index: var(--z-index-notebook-cell-editor-outline); content: ""; @@ -774,100 +464,6 @@ opacity: 0.5 !important; } -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container { - padding-top: 1px !important; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container.emptyNotebook { - opacity: 1 !important; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { - position: absolute; - display: flex; - justify-content: center; - z-index: var(--z-index-notebook-cell-bottom-toolbar-container); - width: calc(100% - 32px); - opacity: 0; - transition: opacity 0.3s ease-in-out; - padding: 0; - margin: 0 16px 0 16px; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { - top: 0px; - height: 33px; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { - display: none; -} - -/* .monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-bottom-toolbar-container, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover .cell-bottom-toolbar-container, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-bottom-toolbar-container, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { - opacity: 1; -} */ - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover, -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { - opacity: 1; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar { - margin-top: 3px; /* This is the minimum to keep the top edge from being cut off at the top of the editor */ -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item { - display: flex; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item.active, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item.active { - transform: none; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { - font-size: 12px; - margin: 0px; - display: inline-flex; - padding: 0px 4px; - border-radius: 0; - align-items: center; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label .codicon, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label .codicon { - margin-right: 3px; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-action-bar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-action-bar { - display: flex; - align-items: center; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { - margin-right: 16px; -} - -.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container span.codicon, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { - text-align: center; - font-size: 14px; -} - -/* markdown */ - .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { height: 20px; width: 20px; @@ -924,6 +520,10 @@ font-style: italic; } +.output-show-more a { + cursor: pointer; +} + .cell-contributed-items.cell-contributed-items-left { margin-left: 4px; } @@ -932,32 +532,21 @@ flex-direction: row-reverse; } -.monaco-workbench .notebookOverlay .output .error .traceback::-webkit-scrollbar, -.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar { - width: 10px; /* width of the entire scrollbar */ - height: 10px; -} - -.monaco-workbench .notebookOverlay .output .error .traceback::-webkit-scrollbar-thumb, -.monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { - height: 10px; - width: 10px; -} - -.monaco-workbench .notebookOverlay .output .error .traceback, -.monaco-workbench .notebookOverlay .output .output-plaintext { - overflow-x: auto; -} - -.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .codicon:not(.suggest-icon) { +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .codicon:not(.suggest-icon) { color: inherit; } +.monaco-workbench .notebookOverlay > .cell-list-container .notebook-overview-ruler-container { + position: absolute; + top: 0; + right: 0; +} + /* high contrast border for multi-select */ -.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-top:before { border-top-style: dotted; } -.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-bottom:before { border-bottom-style: dotted; } -.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before { border-left-style: dotted; } -.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { border-right-style: dotted; } +.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-top:before, .hc-light .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-top:before { border-top-style: dotted; } +.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-bottom:before, .hc-light .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-focus-indicator-bottom:before { border-bottom-style: dotted; } +.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, .hc-light .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before { border-left-style: dotted; } +.hc-black .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before, .hc-light .notebookOverlay .monaco-list.selection-multiple:focus-within .monaco-list-row.selected:not(.focused) .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { border-right-style: dotted; } .monaco-workbench .notebookOverlay .cell-editor-container .monaco-editor .margin-view-overlays .codicon-folding-expanded, .monaco-workbench .notebookOverlay .cell-editor-container .monaco-editor .margin-view-overlays .codicon-folding-collapsed { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css new file mode 100644 index 00000000000..e0003d48f14 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellInsertToolbar.css @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .cell-list-top-cell-toolbar-container { + padding-top: 1px !important; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container.emptyNotebook { + opacity: 1 !important; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { + position: absolute; + display: flex; + justify-content: center; + z-index: var(--z-index-notebook-cell-bottom-toolbar-container); + width: calc(100% - 32px); + opacity: 0; + transition: opacity 0.3s ease-in-out; + padding: 0; + margin: 0 16px 0 16px; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { + top: 0px; + height: 33px; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:focus-within, +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container:hover, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within { + opacity: 1; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar { + margin-top: 3px; /* This is the minimum to keep the top edge from being cut off at the top of the editor */ +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item { + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-item.active, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-item.active { + transform: none; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label { + font-size: 12px; + margin: 0px; + display: inline-flex; + padding: 0px 4px; + border-radius: 0; + align-items: center; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-toolbar .action-label .codicon, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-toolbar .action-label .codicon { + margin-right: 3px; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .monaco-action-bar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .monaco-action-bar { + display: flex; + align-items: center; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container .action-item:first-child, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .action-item:first-child { + margin-right: 16px; +} + +.monaco-workbench .notebookOverlay .cell-list-top-cell-toolbar-container span.codicon, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { + text-align: center; + font-size: 14px; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css new file mode 100644 index 00000000000..8f5eb5dc052 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellStatusBar.css @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .cell-statusbar-container { + height: 22px; + font-size: 12px; + display: flex; + position: relative; + overflow: hidden; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-hidden .cell-statusbar-container { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { + display: flex; + flex-grow: 1; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left, +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { + display: flex; + z-index: var(--z-index-notebook-cell-status); +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items { + justify-content: flex-end; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-contributed-items { + display: flex; + flex-wrap: wrap; + overflow: hidden; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item { + display: flex; + align-items: center; + white-space: pre; + + height: 21px; /* Editor outline is -1px in, don't overlap */ + margin: 0px 3px; + padding: 0px 3px; + overflow: hidden; + text-overflow: clip; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command { + cursor: pointer; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left > .cell-contributed-items { + margin-left: 10px; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .codicon { + font-size: 14px; + color: unset; /* Inherit from parent cell-status-item */ +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item-show-when-active { + display: none; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container.is-active-cell .cell-status-item-show-when-active { + display: initial; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellTitleToolbar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellTitleToolbar.css new file mode 100644 index 00000000000..8023455548e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellTitleToolbar.css @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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 > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + opacity: 0; + display: inline-flex; + position: absolute; + height: 26px; + top: -14px; + /* this lines up the bottom toolbar border with the current line when on line 01 */ + z-index: var(--z-index-notebook-cell-toolbar); +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { + z-index: var(--z-index-notebook-cell-toolbar-dropdown-active); +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item.menu-entry { + width: 24px; + height: 24px; + display: flex; + align-items: center; + margin: 1px 2px; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .action-label { + display: flex; + align-items: center; + margin: auto; +} + + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown { + width: 100%; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item .monaco-dropdown .dropdown-label { + display: flex; +} + +/* toolbar visible on hover */ +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list:focus-within > .monaco-scrollable-element > .monaco-list-rows:not(:hover) > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .markdown-cell-hover.cell-has-toolbar-actions .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar:hover, +.monaco-workbench .notebookOverlay.cell-toolbar-hover > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-toolbar-dropdown-active .cell-title-toolbar { + opacity: 1; +} + +/* toolbar visible on click */ +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + visibility: hidden; +} +.monaco-workbench .notebookOverlay.cell-toolbar-click > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-title-toolbar { + opacity: 1; + visibility: visible; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookFocusIndicator.css b/src/vs/workbench/contrib/notebook/browser/media/notebookFocusIndicator.css new file mode 100644 index 00000000000..a8cf0c9b752 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookFocusIndicator.css @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * 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 .monaco-list .monaco-list-row .cell-focus-indicator-top:before { + top: 0; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left:before { + left: 0; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom:before { + bottom: 0px; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right:before { + right: 0; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator { + position: absolute; + top: 0px; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { + /** Overidden for code cells */ + top: 0px; + bottom: 0px; +} + +.monaco-workbench .notebookOverlay .monaco-list .webview-backed-markdown-cell .cell-focus-indicator-side { + /* Disable pointer events for the folding container */ + pointer-events: none; +} + +.monaco-workbench .notebookOverlay .monaco-list .webview-backed-markdown-cell .cell-focus-indicator-side .notebook-folding-indicator { + /* But allow clicking on the folding indicator itself */ + pointer-events: all; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-bottom { + width: 100%; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-right { + right: 0px; +} + +/** cell border colors */ + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container.cell-editor-focus:before { + border-color: var(notebook-selected-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before { + border-color: var(--notebook-inactive-focused-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-top:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-bottom:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-left:before, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-inner-container:not(.cell-editor-focus) .cell-focus-indicator-right:before { + border-color: var(--notebook-focused-cell-border-color) !important; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator-container { + display: none; + position: relative; + cursor: pointer; + pointer-events: all; /* Take pointer-events in markdown cell */ + width: 11px; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left .codeOutput-focus-indicator { + width: 0px; + height: 100%; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-left, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-inner-container { + cursor: grab; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator .codicon:hover { + cursor: pointer; +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css b/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css new file mode 100644 index 00000000000..4b61bafdfd8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookToolbar.css @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * 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-toolbar-container { + width: 100%; + display: none; + margin-top: 2px; + margin-bottom: 2px; + contain: style; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item { + height: 22px; + display: flex; + align-items: center; + border-radius: 5px; + margin-right: 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element { + flex: 1; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container > .monaco-scrollable-element .notebook-toolbar-left { + padding: 0px 0px 0px 8px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-right { + display: flex; + padding: 0px 0px 0px 0px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .kernel-label { + background-size: 16px; + padding: 0px 5px 0px 3px; + border-radius: 5px; + font-size: 13px; + height: 22px; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .notebook-toolbar-left .monaco-action-bar .action-item .action-label.separator { + margin: 5px 0px !important; + padding: 0px !important; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item:not(.disabled):hover { + background-color: var(--vscode-toolbar-hoverBackground); +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .action-label { + background-size: 16px; + padding-left: 2px; +} + +.monaco-workbench .notebook-action-view-item .action-label { + display: inline-flex; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item .notebook-label { + background-size: 16px; + padding: 0px 5px 0px 2px; + border-radius: 5px; + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar .action-item.disabled .notebook-label { + opacity: 0.4; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active .action-label:not(.disabled) { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-label:not(.disabled):hover { + background-color: unset; +} + +.monaco-workbench .notebookOverlay .notebook-toolbar-container .monaco-action-bar:not(.vertical) .action-item.active { + background-color: unset; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 1b066da0104..619b09c88e0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -88,22 +88,24 @@ import 'vs/workbench/contrib/notebook/browser/contrib/viewportCustomMarkdown/vie import 'vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout'; import 'vs/workbench/contrib/notebook/browser/contrib/breakpoints/notebookBreakpoints'; import 'vs/workbench/contrib/notebook/browser/contrib/execute/executionEditorProgress'; -import 'vs/workbench/contrib/notebook/browser/contrib/execute/execution'; // Diff Editor Contribution import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; -// Output renderers registration +// Services import { editorOptionsRegistry } from 'vs/editor/common/config/editorOptions'; import { NotebookExecutionStateService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionStateServiceImpl'; import { NotebookExecutionService } from 'vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookKeymapService } from 'vs/workbench/contrib/notebook/common/notebookKeymapService'; -import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl'; +import { NotebookKeymapService } from 'vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { NotebookInfo } from 'vs/editor/common/languageFeatureRegistry'; +import { COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentReply'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; /*--------------------------------------------------------------------------------------------- */ @@ -209,6 +211,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri constructor( @IUndoRedoService undoRedoService: IUndoRedoService, @IConfigurationService configurationService: IConfigurationService, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); @@ -222,6 +225,9 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return NotebookContribution._getCellUndoRedoComparisonKey(uri); } })); + + // register comment decoration + this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); } private static _getCellUndoRedoComparisonKey(uri: URI) { @@ -618,10 +624,10 @@ class NotebookLanguageSelectorScoreRefine { @INotebookService private readonly _notebookService: INotebookService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, ) { - languageFeaturesService.setNotebookTypeResolver(this._getNotebookType.bind(this)); + languageFeaturesService.setNotebookTypeResolver(this._getNotebookInfo.bind(this)); } - private _getNotebookType(uri: URI): string | undefined { + private _getNotebookInfo(uri: URI): NotebookInfo | undefined { const cellUri = CellUri.parse(uri); if (!cellUri) { return undefined; @@ -630,7 +636,10 @@ class NotebookLanguageSelectorScoreRefine { if (!notebook) { return undefined; } - return notebook.viewType; + return { + uri: notebook.uri, + type: notebook.viewType + }; } } @@ -703,7 +712,7 @@ configurationRegistry.registerConfiguration({ properties: { [NotebookSetting.displayOrder]: { description: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), - type: ['array'], + type: 'array', items: { type: 'string' }, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index bb51e807a7a..aed8ee62af8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -136,6 +136,7 @@ export interface CodeCellLayoutInfo { readonly editorHeight: number; readonly editorWidth: number; readonly statusBarHeight: number; + readonly commentHeight: number; readonly totalHeight: number; readonly outputContainerOffset: number; readonly outputTotalHeight: number; @@ -150,6 +151,7 @@ export interface CodeCellLayoutInfo { export interface CodeCellLayoutChangeEvent { source?: string; editorHeight?: boolean; + commentHeight?: boolean; outputHeight?: boolean; outputShowMoreContainerHeight?: number; totalHeight?: boolean; @@ -255,6 +257,11 @@ export interface INotebookCellDecorationOptions { gutterClassName?: string; outputClassName?: string; topClassName?: string; + overviewRuler?: { + color: string; + modelRanges: Range[]; + includeOutput: boolean; + }; } export interface INotebookDeltaDecoration { @@ -463,6 +470,11 @@ export interface INotebookEditor { */ createOutput(cell: ICellViewModel, output: IInsetRenderOutput, offset: number): Promise; + /** + * Update the output in webview layer with latest content. It will delegate to `createOutput` is the output is not rendered yet + */ + updateOutput(cell: ICellViewModel, output: IInsetRenderOutput, offset: number): Promise; + readonly onDidReceiveMessage: Event; /** @@ -624,6 +636,7 @@ export interface INotebookEditorDelegate extends INotebookEditor { readonly creationOptions: INotebookEditorCreationOptions; readonly onDidChangeOptions: Event; + readonly onDidChangeDecorations: Event; createMarkupPreview(cell: ICellViewModel): Promise; unhideMarkupPreviews(cells: readonly ICellViewModel[]): Promise; hideMarkupPreviews(cells: readonly ICellViewModel[]): Promise; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index af18bca003d..5420ef359f9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -12,7 +12,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import 'vs/css!./media/notebook'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; import { localize } from 'vs/nls'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 894e3d8a2ff..7662030cee1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -3,6 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/notebook'; +import 'vs/css!./media/notebookCellInsertToolbar'; +import 'vs/css!./media/notebookCellStatusBar'; +import 'vs/css!./media/notebookCellTitleToolbar'; +import 'vs/css!./media/notebookFocusIndicator'; +import 'vs/css!./media/notebookToolbar'; import { PixelRatio } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -19,7 +25,6 @@ import { setTimeout0 } from 'vs/base/common/platform'; import { extname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; -import 'vs/css!./media/notebook'; import { FontMeasurements } from 'vs/editor/browser/config/fontMeasurements'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -50,7 +55,7 @@ import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLog import { NotebookCellStateChangedEvent, NotebookLayoutChangedEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys'; import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; -import { NotebookCellList, NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { ListViewInfoAccessor, NotebookCellList, NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; import { CodeCellRenderer, MarkupCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; @@ -63,6 +68,7 @@ import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/vie import { NotebookDecorationCSSRules, NotebookRefCountedStyleSheet } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorDecorations'; import { NotebookEditorToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar'; import { NotebookEditorContextKeys } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys'; +import { NotebookOverviewRuler } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler'; import { ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookTopCellToolbar'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, INotebookSearchOptions, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -77,150 +83,15 @@ import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; +import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; const $ = DOM.$; -export class ListViewInfoAccessor extends Disposable { - constructor( - readonly list: INotebookCellList - ) { - super(); - } +export function getDefaultNotebookCreationOptions(): INotebookEditorCreationOptions { + // We inlined the id to avoid loading comment contrib in tests + const skipContributions = ['editor.contrib.review']; + const contributions = EditorExtensionsRegistry.getEditorContributions().filter(c => skipContributions.indexOf(c.id) === -1); - setScrollTop(scrollTop: number) { - this.list.scrollTop = scrollTop; - } - - isScrolledToBottom() { - return this.list.isScrolledToBottom(); - } - - scrollToBottom() { - this.list.scrollToBottom(); - } - - revealCellRangeInView(range: ICellRange) { - return this.list.revealElementsInView(range); - } - - revealInView(cell: ICellViewModel) { - this.list.revealElementInView(cell); - } - - revealInViewAtTop(cell: ICellViewModel) { - this.list.revealElementInViewAtTop(cell); - } - - revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this.list.revealElementInCenterIfOutsideViewport(cell); - } - - async revealInCenterIfOutsideViewportAsync(cell: ICellViewModel) { - return this.list.revealElementInCenterIfOutsideViewportAsync(cell); - } - - revealInCenter(cell: ICellViewModel) { - this.list.revealElementInCenter(cell); - } - - async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { - return this.list.revealElementLineInViewAsync(cell, line); - } - - async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise { - return this.list.revealElementLineInCenterAsync(cell, line); - } - - async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise { - return this.list.revealElementLineInCenterIfOutsideViewportAsync(cell, line); - } - - async revealRangeInViewAsync(cell: ICellViewModel, range: Range): Promise { - return this.list.revealElementRangeInViewAsync(cell, range); - } - - async revealRangeInCenterAsync(cell: ICellViewModel, range: Range): Promise { - return this.list.revealElementRangeInCenterAsync(cell, range); - } - - async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise { - return this.list.revealElementRangeInCenterIfOutsideViewportAsync(cell, range); - } - - async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise { - return this.list.revealElementOffsetInCenterAsync(cell, offset); - } - - getViewIndex(cell: ICellViewModel): number { - return this.list.getViewIndex(cell) ?? -1; - } - - getViewHeight(cell: ICellViewModel): number { - if (!this.list.viewModel) { - return -1; - } - - return this.list.elementHeight(cell); - } - - getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined { - if (!this.list.viewModel) { - return undefined; - } - - const modelIndex = this.list.getModelIndex2(startIndex); - if (modelIndex === undefined) { - throw new Error(`startIndex ${startIndex} out of boundary`); - } - - if (endIndex >= this.list.length) { - // it's the end - const endModelIndex = this.list.viewModel.length; - return { start: modelIndex, end: endModelIndex }; - } else { - const endModelIndex = this.list.getModelIndex2(endIndex); - if (endModelIndex === undefined) { - throw new Error(`endIndex ${endIndex} out of boundary`); - } - return { start: modelIndex, end: endModelIndex }; - } - } - - getCellsFromViewRange(startIndex: number, endIndex: number): ReadonlyArray { - if (!this.list.viewModel) { - return []; - } - - const range = this.getCellRangeFromViewRange(startIndex, endIndex); - if (!range) { - return []; - } - - return this.list.viewModel.getCellsInRange(range); - } - - getCellsInRange(range?: ICellRange): ReadonlyArray { - return this.list.viewModel?.getCellsInRange(range) ?? []; - } - - setCellEditorSelection(cell: ICellViewModel, range: Range): void { - this.list.setCellSelection(cell, range); - } - - setHiddenAreas(_ranges: ICellRange[]): boolean { - return this.list.setHiddenAreas(_ranges, true); - } - - getVisibleRangesPlusViewportBelow(): ICellRange[] { - return this.list?.getVisibleRangesPlusViewportBelow() ?? []; - } - - triggerScroll(event: IMouseWheelEvent) { - this.list.triggerScrollFromMouseWheelEvent(event); - } -} - -export function getDefaultNotebookCreationOptions() { return { menuIds: { notebookToolbar: MenuId.NotebookToolbar, @@ -229,7 +100,8 @@ export function getDefaultNotebookCreationOptions() { cellTopInsertToolbar: MenuId.NotebookCellListTop, cellExecuteToolbar: MenuId.NotebookCellExecute, cellExecutePrimary: MenuId.NotebookCellExecutePrimary, - } + }, + cellEditorContributions: contributions }; } @@ -243,8 +115,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidChangeModel: Event = this._onDidChangeModel.event; private readonly _onDidChangeOptions = this._register(new Emitter()); readonly onDidChangeOptions: Event = this._onDidChangeOptions.event; + private readonly _onDidChangeDecorations = this._register(new Emitter()); + readonly onDidChangeDecorations: Event = this._onDidChangeDecorations.event; private readonly _onDidScroll = this._register(new Emitter()); readonly onDidScroll: Event = this._onDidScroll.event; + private readonly _onDidChangeContentHeight = this._register(new Emitter()); + readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; private readonly _onDidChangeActiveCell = this._register(new Emitter()); readonly onDidChangeActiveCell: Event = this._onDidChangeActiveCell.event; private readonly _onDidChangeSelection = this._register(new Emitter()); @@ -273,6 +149,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; private _notebookTopToolbar!: NotebookEditorToolbar; + private _notebookOverviewRulerContainer!: HTMLElement; + private _notebookOverviewRuler!: NotebookOverviewRuler; private _body!: HTMLElement; private _styleElement!: HTMLStyleElement; private _overflowContainer!: HTMLElement; @@ -425,7 +303,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.markupFontSize || e.insertToolbarAlignment) { this._styleElement?.remove(); this._createLayoutStyles(); - this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + this._webview?.updateOptions({ + ...this.notebookOptions.computeWebviewOptions(), + fontFamily: this._generateFontFamily() + }); } if (this._dimension && this._isVisible) { @@ -607,15 +488,25 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD DOM.append(parent, this._notebookTopToolbarContainer); this._body = document.createElement('div'); DOM.append(parent, this._body); + this._body.classList.add('cell-list-container'); this._createLayoutStyles(); this._createCellList(); + this._notebookOverviewRulerContainer = document.createElement('div'); + this._notebookOverviewRulerContainer.classList.add('notebook-overview-ruler-container'); + this._list.scrollableElement.appendChild(this._notebookOverviewRulerContainer); + this._registerNotebookOverviewRuler(); + this._overflowContainer = document.createElement('div'); this._overflowContainer.classList.add('notebook-overflow-widget-container', 'monaco-editor'); DOM.append(parent, this._overflowContainer); } + private _generateFontFamily() { + return this._fontInfo?.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; + } + private _createLayoutStyles(): void { this._styleElement = DOM.createStyleSheet(this._body); const { @@ -647,7 +538,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._generateFontInfo(); } - const fontFamily = this._fontInfo?.fontFamily ?? `"SF Mono", Monaco, Menlo, Consolas, "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace`; + const fontFamily = this._generateFontFamily(); styleSheets.push(` :root { @@ -807,6 +698,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD styleSheets.push(`.notebookOverlay .output { margin: 0px ${cellRightMargin}px 0px ${codeCellLeftMargin + cellRunGutter}px; }`); styleSheets.push(`.notebookOverlay .output { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + // comment + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-comment-container { left: ${codeCellLeftMargin + cellRunGutter}px; }`); + styleSheets.push(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-comment-container { width: calc(100% - ${codeCellLeftMargin + cellRunGutter + cellRightMargin}px); }`); + // output collapse button styleSheets.push(`.monaco-workbench .notebookOverlay .output .output-collapse-container .expandButton { left: -${cellRunGutter}px; }`); styleSheets.push(`.monaco-workbench .notebookOverlay .output .output-collapse-container .expandButton { @@ -1042,6 +937,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD }); } + private _registerNotebookOverviewRuler() { + this._notebookOverviewRuler = this._register(this.instantiationService.createInstance(NotebookOverviewRuler, this, this._notebookOverviewRulerContainer!)); + } + private _registerNotebookActionsToolbar() { this._notebookTopToolbar = this._register(this.instantiationService.createInstance(NotebookEditorToolbar, this, this.scopedContextKeyService, this._notebookOptions, this._notebookTopToolbarContainer)); this._register(this._notebookTopToolbar.onDidChangeState(() => { @@ -1098,7 +997,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD || oldBottomToolbarDimensions.bottomToolbarHeight !== newBottomToolbarDimensions.bottomToolbarHeight) { this._styleElement?.remove(); this._createLayoutStyles(); - this._webview?.updateOptions(this.notebookOptions.computeWebviewOptions()); + this._webview?.updateOptions({ + ...this.notebookOptions.computeWebviewOptions(), + fontFamily: this._generateFontFamily() + }); } type WorkbenchNotebookOpenClassification = { scheme: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -1348,7 +1250,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD didDragMarkupCell: that._didDragMarkupCell.bind(that), didDropMarkupCell: that._didDropMarkupCell.bind(that), didEndDragMarkupCell: that._didEndDragMarkupCell.bind(that) - }, id, resource, this._notebookOptions.computeWebviewOptions(), this.notebookRendererMessaging.getScoped(this._uuid)); + }, id, resource, { + ...this._notebookOptions.computeWebviewOptions(), + fontFamily: this._generateFontFamily() + }, this.notebookRendererMessaging.getScoped(this._uuid)); this._webview.element.style.width = '100%'; @@ -1405,6 +1310,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD DOM.scheduleAtNextAnimationFrame(() => { hasPendingChangeContentHeight = false; this._updateScrollHeight(); + this._onDidChangeContentHeight.fire(this._list.getScrollHeight()); }, 100); })); @@ -1768,6 +1674,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } this._notebookTopToolbar.layout(this._dimension); + this._notebookOverviewRuler.layout(); this._viewContext?.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); } @@ -2069,7 +1976,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { - return this.viewModel?.deltaCellDecorations(oldDecorations, newDecorations) || []; + const ret = this.viewModel?.deltaCellDecorations(oldDecorations, newDecorations) || []; + this._onDidChangeDecorations.fire(); + return ret; } deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]) { @@ -2491,6 +2400,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return { width: this._dimension?.width ?? 0, height: this._dimension?.height ?? 0, + scrollHeight: this._list?.getScrollHeight() ?? 0, fontInfo: this._fontInfo! }; } @@ -2628,6 +2538,36 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD }); } + async updateOutput(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { + this._insetModifyQueueByOutputId.queue(output.source.model.outputId, async () => { + if (!this._webview) { + return; + } + + if (!this._webview.isResolved()) { + await this._resolveWebview(); + } + + if (!this._webview || !this._list.webviewElement) { + return; + } + + if (!this._webview.insetMapping.has(output.source)) { + return this.createOutput(cell, output, offset); + } + + if (output.type === RenderOutputType.Extension) { + this.notebookRendererMessaging.prepare(output.renderer.id); + } + + const webviewTop = parseInt(this._list.webviewElement.domNode.style.top, 10); + const top = !!webviewTop ? (0 - webviewTop) : 0; + + const cellTop = this._list.getAbsoluteTopOfElement(cell) + top; + await this._webview.updateOutput({ cellId: cell.id, cellHandle: cell.handle, cellUri: cell.uri }, output, cellTop, offset); + }); + } + removeInset(output: ICellOutputViewModel) { this._insetModifyQueueByOutputId.queue(output.model.outputId, async () => { if (this._webview?.isResolved()) { @@ -2909,135 +2849,157 @@ registerZIndex(ZIndex.Sash, 3, 'notebook-cell-toolbar-dropdown-active'); export const notebookCellBorder = registerColor('notebook.cellBorderColor', { dark: transparent(listInactiveSelectionBackground, 1), light: transparent(listInactiveSelectionBackground, 1), - hc: PANEL_BORDER + hcDark: PANEL_BORDER, + hcLight: PANEL_BORDER }, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); export const focusedEditorBorderColor = registerColor('notebook.focusedEditorBorder', { light: focusBorder, dark: focusBorder, - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, nls.localize('notebook.focusedEditorBorder', "The color of the notebook cell editor border.")); export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', { light: debugIconStartForeground, dark: debugIconStartForeground, - hc: debugIconStartForeground + hcDark: debugIconStartForeground, + hcLight: debugIconStartForeground }, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', { light: errorForeground, dark: errorForeground, - hc: errorForeground + hcDark: errorForeground, + hcLight: errorForeground }, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', { light: foreground, dark: foreground, - hc: foreground + hcDark: foreground, + hcLight: foreground }, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); export const notebookOutputContainerBorderColor = registerColor('notebook.outputContainerBorderColor', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, nls.localize('notebook.outputContainerBorderColor', "The border color of the notebook output container.")); export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, nls.localize('notebook.outputContainerBackgroundColor', "The color of the notebook output container background.")); // TODO@rebornix currently also used for toolbar border, if we keep all of this, pick a generic name export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeparator', { dark: Color.fromHex('#808080').transparent(0.35), light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, nls.localize('notebook.cellToolbarSeparator', "The color of the separator in the cell bottom toolbar")); export const focusedCellBackground = registerColor('notebook.focusedCellBackground', { dark: null, light: null, - hc: null + hcDark: null, + hcLight: null }, nls.localize('focusedCellBackground', "The background color of a cell when the cell is focused.")); export const selectedCellBackground = registerColor('notebook.selectedCellBackground', { dark: listInactiveSelectionBackground, light: listInactiveSelectionBackground, - hc: null + hcDark: null, + hcLight: null }, nls.localize('selectedCellBackground', "The background color of a cell when the cell is selected.")); export const cellHoverBackground = registerColor('notebook.cellHoverBackground', { dark: transparent(focusedCellBackground, .5), light: transparent(focusedCellBackground, .7), - hc: null + hcDark: null, + hcLight: null }, nls.localize('notebook.cellHoverBackground', "The background color of a cell when the cell is hovered.")); export const selectedCellBorder = registerColor('notebook.selectedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, - hc: contrastBorder + hcDark: contrastBorder, + hcLight: contrastBorder }, nls.localize('notebook.selectedCellBorder', "The color of the cell's top and bottom border when the cell is selected but not focused.")); export const inactiveSelectedCellBorder = registerColor('notebook.inactiveSelectedCellBorder', { dark: null, light: null, - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, nls.localize('notebook.inactiveSelectedCellBorder', "The color of the cell's borders when multiple cells are selected.")); export const focusedCellBorder = registerColor('notebook.focusedCellBorder', { dark: focusBorder, light: focusBorder, - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, nls.localize('notebook.focusedCellBorder', "The color of the cell's focus indicator borders when the cell is focused.")); export const inactiveFocusedCellBorder = registerColor('notebook.inactiveFocusedCellBorder', { dark: notebookCellBorder, light: notebookCellBorder, - hc: notebookCellBorder + hcDark: notebookCellBorder, + hcLight: notebookCellBorder }, nls.localize('notebook.inactiveFocusedCellBorder', "The color of the cell's top and bottom border when a cell is focused while the primary focus is outside of the editor.")); export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { light: new Color(new RGBA(0, 0, 0, 0.08)), dark: new Color(new RGBA(255, 255, 255, 0.15)), - hc: new Color(new RGBA(255, 255, 255, 0.15)), + hcDark: new Color(new RGBA(255, 255, 255, 0.15)), + hcLight: new Color(new RGBA(0, 0, 0, 0.08)), }, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); export const cellInsertionIndicator = registerColor('notebook.cellInsertionIndicator', { light: focusBorder, dark: focusBorder, - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, nls.localize('notebook.cellInsertionIndicator', "The color of the notebook cell insertion indicator.")); export const listScrollbarSliderBackground = registerColor('notebookScrollbarSlider.background', { dark: scrollbarSliderBackground, light: scrollbarSliderBackground, - hc: scrollbarSliderBackground + hcDark: scrollbarSliderBackground, + hcLight: scrollbarSliderBackground }, nls.localize('notebookScrollbarSliderBackground', "Notebook scrollbar slider background color.")); export const listScrollbarSliderHoverBackground = registerColor('notebookScrollbarSlider.hoverBackground', { dark: scrollbarSliderHoverBackground, light: scrollbarSliderHoverBackground, - hc: scrollbarSliderHoverBackground + hcDark: scrollbarSliderHoverBackground, + hcLight: scrollbarSliderHoverBackground }, nls.localize('notebookScrollbarSliderHoverBackground', "Notebook scrollbar slider background color when hovering.")); export const listScrollbarSliderActiveBackground = registerColor('notebookScrollbarSlider.activeBackground', { dark: scrollbarSliderActiveBackground, light: scrollbarSliderActiveBackground, - hc: scrollbarSliderActiveBackground + hcDark: scrollbarSliderActiveBackground, + hcLight: scrollbarSliderActiveBackground }, nls.localize('notebookScrollbarSliderActiveBackground', "Notebook scrollbar slider background color when clicked on.")); export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackground', { dark: Color.fromHex('#ffffff0b'), light: Color.fromHex('#fdff0033'), - hc: null + hcDark: null, + hcLight: null }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); export const cellEditorBackground = registerColor('notebook.cellEditorBackground', { light: SIDE_BAR_BACKGROUND, dark: SIDE_BAR_BACKGROUND, - hc: null + hcDark: null, + hcLight: null }, nls.localize('notebook.cellEditorBackground', "Cell editor background color.")); registerThemingParticipant((theme, collector) => { @@ -3206,8 +3168,6 @@ registerThemingParticipant((theme, collector) => { const scrollbarSliderHoverBackgroundColor = theme.getColor(listScrollbarSliderHoverBackground); if (scrollbarSliderHoverBackgroundColor) { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider:hover { background: ${scrollbarSliderHoverBackgroundColor}; } `); - collector.addRule(` .monaco-workbench .notebookOverlay .output-plaintext::-webkit-scrollbar-thumb { background: ${scrollbarSliderHoverBackgroundColor}; } `); - collector.addRule(` .monaco-workbench .notebookOverlay .output .error .traceback::-webkit-scrollbar-thumb { background: ${scrollbarSliderHoverBackgroundColor}; } `); } const scrollbarSliderActiveBackgroundColor = theme.getColor(listScrollbarSliderActiveBackground); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index d2d9d2c7ed1..1999bf283bc 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -192,8 +192,8 @@ export class NotebookProviderInfoStore extends Disposable { return { editor: NotebookEditorInput.create(this._instantiationService, ref.object.resource, notebookProviderInfo.id), options }; }; - const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = ({ modified, original }) => { - return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, undefined, undefined, original.resource!, notebookProviderInfo.id) }; + const notebookDiffEditorInputFactory: DiffEditorInputFactoryFunction = ({ modified, original, label, description }) => { + return { editor: NotebookDiffEditorInput.create(this._instantiationService, modified.resource!, label, description, original.resource!, notebookProviderInfo.id) }; }; // Register the notebook editor disposables.add(this._editorResolverService.registerEditor( diff --git a/src/vs/workbench/contrib/notebook/browser/notebookViewEvents.ts b/src/vs/workbench/contrib/notebook/browser/notebookViewEvents.ts index c8dd67db793..15b684b1760 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookViewEvents.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookViewEvents.ts @@ -10,6 +10,7 @@ import { NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/n export interface NotebookLayoutInfo { width: number; height: number; + scrollHeight: number; fontInfo: FontInfo; } @@ -28,6 +29,7 @@ export interface CellViewModelStateChangeEvent { readonly cellLineNumberChanged?: boolean; readonly inputCollapsedChanged?: boolean; readonly outputCollapsedChanged?: boolean; + readonly dragStateChanged?: boolean; } export interface NotebookLayoutChangeEvent { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts similarity index 100% rename from src/vs/workbench/contrib/notebook/browser/notebookKeymapServiceImpl.ts rename to src/vs/workbench/contrib/notebook/browser/services/notebookKeymapServiceImpl.ts diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts similarity index 65% rename from src/vs/workbench/contrib/notebook/browser/view/cellParts/cellPart.ts rename to src/vs/workbench/contrib/notebook/browser/view/cellPart.ts index a8740668eee..ecebf3c9765 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellPart.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { ICellExecutionStateChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; export abstract class CellPart extends Disposable { + protected currentCell: ICellViewModel | undefined; + protected cellDisposables = new DisposableStore(); + constructor() { super(); } @@ -17,24 +19,37 @@ export abstract class CellPart extends Disposable { /** * Update the DOM for the cell `element` */ - abstract renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void; + renderCell(element: ICellViewModel): void { + this.currentCell = element; + this.didRenderCell(element); + } + + protected didRenderCell(element: ICellViewModel): void { } + + /** + * Dispose any disposables generated from `didRenderCell` + */ + unrenderCell(element: ICellViewModel): void { + this.currentCell = undefined; + this.cellDisposables.clear(); + } /** * Perform DOM read operations to prepare for the list/cell layout update. */ - abstract prepareLayout(): void; + prepareLayout(): void { } /** * Update internal DOM (top positions) per cell layout info change * Note that a cell part doesn't need to call `DOM.scheduleNextFrame`, * the list view will ensure that layout call is invoked in the right frame */ - abstract updateInternalLayoutNow(element: ICellViewModel): void; + updateInternalLayoutNow(element: ICellViewModel): void { } /** * Update per cell state change */ - abstract updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void; + updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { } /** * Update per execution state change. diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts new file mode 100644 index 00000000000..f09b8767e87 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import * as languages from 'vs/editor/common/languages'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { CommentThreadWidget } from 'vs/workbench/contrib/comments/browser/commentThreadWidget'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; +import { coalesce } from 'vs/base/common/arrays'; +import { peekViewBorder, peekViewResultsBackground } from 'vs/editor/contrib/peekView/browser/peekView'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; + +export class CellComments extends CellPart { + private _initialized: boolean = false; + private _commentThreadWidget: CommentThreadWidget | null = null; + private currentElement: CodeCellViewModel | undefined; + private readonly commentTheadDisposables = this._register(new DisposableStore()); + + constructor( + private readonly notebookEditor: INotebookEditorDelegate, + private readonly container: HTMLElement, + + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IThemeService private readonly themeService: IThemeService, + @ICommentService private readonly commentService: ICommentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.container.classList.add('review-widget'); + + this._register(this.themeService.onDidColorThemeChange(this._applyTheme, this)); + // TODO @rebornix onDidChangeLayout (font change) + // this._register(this.notebookEditor.onDidchangeLa) + this._applyTheme(); + } + + private async initialize(element: ICellViewModel) { + if (this._initialized) { + return; + } + + this._initialized = true; + const info = await this._getCommentThreadForCell(element); + + if (info) { + this._createCommentTheadWidget(info.owner, info.thread); + } + } + + private _createCommentTheadWidget(owner: string, commentThread: languages.CommentThread) { + this._commentThreadWidget?.dispose(); + this.commentTheadDisposables.clear(); + this._commentThreadWidget = this.instantiationService.createInstance( + CommentThreadWidget, + this.container, + owner, + this.notebookEditor.textModel!.uri, + this.contextKeyService, + this.instantiationService, + commentThread, + null, + { + codeBlockFontFamily: this.configurationService.getValue('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily + }, + undefined, + { + actionRunner: () => { + }, + collapse: () => { } + } + ) as unknown as CommentThreadWidget; + + const layoutInfo = this.notebookEditor.getLayoutInfo(); + + this._commentThreadWidget.display(layoutInfo.fontInfo.lineHeight); + this._applyTheme(); + + this.commentTheadDisposables.add(this._commentThreadWidget.onDidResize(() => { + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { + this.currentElement.commentHeight = dom.getClientArea(this._commentThreadWidget.container).height; + } + })); + } + + private _bindListeners() { + this.cellDisposables.add(this.commentService.onDidUpdateCommentThreads(async () => { + if (this.currentElement) { + const info = await this._getCommentThreadForCell(this.currentElement); + if (!this._commentThreadWidget && info) { + this._createCommentTheadWidget(info.owner, info.thread); + const layoutInfo = (this.currentElement as CodeCellViewModel).layoutInfo; + this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; + + this.currentElement.commentHeight = dom.getClientArea(this._commentThreadWidget!.container).height; + + return; + } + + if (this._commentThreadWidget) { + if (info) { + this._commentThreadWidget.updateCommentThread(info.thread); + this.currentElement.commentHeight = dom.getClientArea(this._commentThreadWidget.container).height; + } else { + this._commentThreadWidget.dispose(); + this.currentElement.commentHeight = 0; + } + } + } + })); + } + + private async _getCommentThreadForCell(element: ICellViewModel): Promise<{ thread: languages.CommentThread; owner: string } | null> { + if (this.notebookEditor.hasModel()) { + const commentInfos = coalesce(await this.commentService.getNotebookComments(element.uri)); + if (commentInfos.length && commentInfos[0].threads.length) { + return { owner: commentInfos[0].owner, thread: commentInfos[0].threads[0] }; + } + } + + return null; + } + + private _applyTheme() { + const theme = this.themeService.getColorTheme(); + const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + this._commentThreadWidget?.applyTheme(theme, fontInfo); + } + + protected override didRenderCell(element: ICellViewModel): void { + if (element.cellKind === CellKind.Code) { + this.currentElement = element as CodeCellViewModel; + this.initialize(element); + this._bindListeners(); + } + + } + + override prepareLayout(): void { + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { + this.currentElement.commentHeight = dom.getClientArea(this._commentThreadWidget.container).height; + } + } + + override updateInternalLayoutNow(element: ICellViewModel): void { + if (this.currentElement?.cellKind === CellKind.Code && this._commentThreadWidget) { + const layoutInfo = (element as CodeCellViewModel).layoutInfo; + this.container.style.top = `${layoutInfo.outputContainerOffset + layoutInfo.outputTotalHeight}px`; + } + } +} + +registerThemingParticipant((theme, collector) => { + const borderColor = theme.getColor(peekViewBorder); + + if (borderColor) { + collector.addRule(`.cell-comment-container.review-widget { border-left: 1px solid ${borderColor}; border-right: 1px solid ${borderColor}; }`); + collector.addRule(`.cell-comment-container.review-widget > .head { border-top: 1px solid ${borderColor}; }`); + collector.addRule(`.cell-comment-container.review-widget > .body { border-bottom: 1px solid ${borderColor}; }`); + } + + const peekViewBackground = theme.getColor(peekViewResultsBackground); + if (peekViewBackground) { + collector.addRule( + `.cell-comment-container.review-widget {` + + ` background-color: ${peekViewBackground};` + + `}`); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index 55f58c4ac72..e1e2a982be5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -12,6 +12,25 @@ import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewM import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +export class CellContextKeyPart extends CellPart { + private cellContextKeyManager: CellContextKeyManager; + + constructor( + notebookEditor: INotebookEditorDelegate, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.cellContextKeyManager = this._register(this.instantiationService.createInstance(CellContextKeyManager, notebookEditor, undefined)); + } + + protected override didRenderCell(element: ICellViewModel): void { + this.cellContextKeyManager.updateForElement(element); + } +} export class CellContextKeyManager extends Disposable { @@ -32,7 +51,7 @@ export class CellContextKeyManager extends Disposable { constructor( private readonly notebookEditor: INotebookEditorDelegate, - private element: ICellViewModel, + private element: ICellViewModel | undefined, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService ) { @@ -51,18 +70,26 @@ export class CellContextKeyManager extends Disposable { this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); - this.updateForElement(element); + if (element) { + this.updateForElement(element); + } }); this._register(this._notebookExecutionStateService.onDidChangeCellExecution(e => { - if (e.affectsCell(this.element.uri)) { + if (this.element && e.affectsCell(this.element.uri)) { this.updateForExecutionState(); } })); } - public updateForElement(element: ICellViewModel) { + public updateForElement(element: ICellViewModel | undefined) { this.elementDisposables.clear(); + this.element = element; + + if (!element) { + return; + } + this.elementDisposables.add(element.onDidChangeState(e => this.onDidChangeState(e))); if (element instanceof CodeCellViewModel) { @@ -71,7 +98,6 @@ export class CellContextKeyManager extends Disposable { this.elementDisposables.add(this.notebookEditor.onDidChangeActiveCell(() => this.updateForFocusState())); - this.element = element; if (this.element instanceof MarkupCellViewModel) { this.cellType.set('markup'); } else if (this.element instanceof CodeCellViewModel) { @@ -85,7 +111,7 @@ export class CellContextKeyManager extends Disposable { this.updateForCollapseState(); this.updateForOutputs(); - this.cellLineNumbers.set(this.element.lineNumbers); + this.cellLineNumbers.set(this.element!.lineNumbers); }); } @@ -104,7 +130,7 @@ export class CellContextKeyManager extends Disposable { } if (e.cellLineNumberChanged) { - this.cellLineNumbers.set(this.element.lineNumbers); + this.cellLineNumbers.set(this.element!.lineNumbers); } if (e.inputCollapsedChanged || e.outputCollapsedChanged) { @@ -114,6 +140,10 @@ export class CellContextKeyManager extends Disposable { } private updateForFocusState() { + if (!this.element) { + return; + } + const activeCell = this.notebookEditor.getActiveCell(); this.cellFocused.set(this.notebookEditor.getActiveCell() === this.element); @@ -126,6 +156,10 @@ export class CellContextKeyManager extends Disposable { } private updateForExecutionState() { + if (!this.element) { + return; + } + const internalMetadata = this.element.internalMetadata; this.cellEditable.set(!this.notebookEditor.isReadOnly); @@ -152,6 +186,10 @@ export class CellContextKeyManager extends Disposable { } private updateForEditState() { + if (!this.element) { + return; + } + if (this.element instanceof MarkupCellViewModel) { this.markdownEditMode.set(this.element.getEditState() === CellEditState.Editing); } else { @@ -160,6 +198,10 @@ export class CellContextKeyManager extends Disposable { } private updateForCollapseState() { + if (!this.element) { + return; + } + this.cellContentCollapsed.set(!!this.element.isInputCollapsed); this.cellOutputCollapsed.set(!!this.element.isOutputCollapsed); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts index d2f8fe10881..7aac88fa2dc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations.ts @@ -4,39 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { Disposable } from 'vs/base/common/lifecycle'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -export class CellDecorations extends Disposable { +export class CellDecorations extends CellPart { constructor( - rootContainer: HTMLElement, - decorationContainer: HTMLElement, - element: ICellViewModel + readonly rootContainer: HTMLElement, + readonly decorationContainer: HTMLElement, ) { super(); + } + protected override didRenderCell(element: ICellViewModel): void { const removedClassNames: string[] = []; - rootContainer.classList.forEach(className => { + this.rootContainer.classList.forEach(className => { if (/^nb\-.*$/.test(className)) { removedClassNames.push(className); } }); removedClassNames.forEach(className => { - rootContainer.classList.remove(className); + this.rootContainer.classList.remove(className); }); - decorationContainer.innerText = ''; + this.decorationContainer.innerText = ''; const generateCellTopDecorations = () => { - decorationContainer.innerText = ''; + this.decorationContainer.innerText = ''; element.getCellDecorations().filter(options => options.topClassName !== undefined).forEach(options => { - decorationContainer.append(DOM.$(`.${options.topClassName!}`)); + this.decorationContainer.append(DOM.$(`.${options.topClassName!}`)); }); }; - this._register(element.onCellDecorationsChanged((e) => { + this.cellDisposables.add(element.onCellDecorationsChanged((e) => { const modified = e.added.find(e => e.topClassName) || e.removed.find(e => e.topClassName); if (modified) { 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 de90b76a0ff..3ad3f9ad1fa 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd.ts @@ -8,9 +8,11 @@ import { Delayer } from 'vs/base/common/async'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { expandCellRangesWithHiddenCells, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { BaseCellRenderTemplate, INotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { cloneNotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellEditType, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellEditType, ICellMoveEdit, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; const $ = DOM.$; @@ -28,10 +30,33 @@ interface CellDragEvent { dragPosRatio: number; } +export class CellDragAndDropPart extends CellPart { + constructor( + private readonly container: HTMLElement + ) { + super(); + } + + override didRenderCell(element: ICellViewModel): void { + this.update(element); + } + + override updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { + if (e.dragStateChanged) { + this.update(element); + } + } + + private update(element: ICellViewModel) { + this.container.classList.toggle(DRAGGING_CLASS, element.dragging); + } +} + export class CellDragAndDropController extends Disposable { // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need // to figure out how to prevent that private currentDraggedCell: ICellViewModel | undefined; + private draggedCells: ICellViewModel[] = []; private listInsertionIndicator: HTMLElement; @@ -44,11 +69,11 @@ export class CellDragAndDropController extends Disposable { constructor( private readonly notebookEditor: INotebookEditorDelegate, - insertionIndicatorContainer: HTMLElement + private readonly notebookListContainer: HTMLElement ) { super(); - this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); + this.listInsertionIndicator = DOM.append(notebookListContainer, $('.cell-list-insertion-indicator')); this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_START, this.onGlobalDragStart.bind(this), true)); this._register(DOM.addDisposableListener(document.body, DOM.EventType.DRAG_END, this.onGlobalDragEnd.bind(this), true)); @@ -97,20 +122,12 @@ export class CellDragAndDropController extends Disposable { }); } - renderElement(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { - if (element.dragging) { - templateData.container.classList.add(DRAGGING_CLASS); - } else { - templateData.container.classList.remove(DRAGGING_CLASS); - } - } - private setInsertIndicatorVisibility(visible: boolean) { this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; } private toCellDragEvent(event: DragEvent): CellDragEvent | undefined { - const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; + const targetTop = this.notebookListContainer.getBoundingClientRect().top; const dragOffset = this.list.scrollTop + event.clientY - targetTop; const draggedOverCell = this.list.elementAt(dragOffset); if (!draggedOverCell) { @@ -257,40 +274,7 @@ export class CellDragAndDropController extends Disposable { ], true, { kind: SelectionStateType.Index, focus: this.notebookEditor.getFocus(), selections: this.notebookEditor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); this.notebookEditor.revealCellRangeInView(finalSelection); } else { - const draggedCellIndex = this.notebookEditor.getCellIndex(draggedCell); - const range = this.getCellRangeAroundDragTarget(draggedCellIndex); - let originalToIdx = this.notebookEditor.getCellIndex(draggedOverCell); - if (dropDirection === 'below') { - const relativeToIndex = this.notebookEditor.getCellIndex(draggedOverCell); - const newIdx = this.notebookEditor.getNextVisibleCellIndex(relativeToIndex); - originalToIdx = newIdx; - } - - if (originalToIdx >= range.start && originalToIdx <= range.end) { - return; - } - - let finalSelection: ICellRange; - let finalFocus: ICellRange; - - if (originalToIdx <= range.start) { - finalSelection = { start: originalToIdx, end: originalToIdx + range.end - range.start }; - finalFocus = { start: originalToIdx + draggedCellIndex - range.start, end: originalToIdx + draggedCellIndex - range.start + 1 }; - } else { - const delta = (originalToIdx - range.end); - finalSelection = { start: range.start + delta, end: range.end + delta }; - finalFocus = { start: draggedCellIndex + delta, end: draggedCellIndex + delta + 1 }; - } - - textModel.applyEdits([ - { - editType: CellEditType.Move, - index: range.start, - length: range.end - range.start, - newIdx: originalToIdx <= range.start ? originalToIdx : (originalToIdx - (range.end - range.start)) - } - ], true, { kind: SelectionStateType.Index, focus: this.notebookEditor.getFocus(), selections: this.notebookEditor.getSelections() }, () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), undefined, true); - this.notebookEditor.revealCellRangeInView(finalSelection); + performCellDropEdits(this.notebookEditor, draggedCell, dropDirection, draggedOverCell); } } @@ -302,8 +286,9 @@ export class CellDragAndDropController extends Disposable { private dragCleanup(): void { if (this.currentDraggedCell) { - this.currentDraggedCell.dragging = false; + this.draggedCells.forEach(cell => cell.dragging = false); this.currentDraggedCell = undefined; + this.draggedCells = []; } this.setInsertIndicatorVisibility(false); @@ -338,14 +323,13 @@ export class CellDragAndDropController extends Disposable { } this.currentDraggedCell = templateData.currentRenderedCell!; - this.currentDraggedCell.dragging = true; + this.draggedCells = this.notebookEditor.getSelections().map(range => this.notebookEditor.getCellsInRange(range)).flat(); + this.draggedCells.forEach(cell => cell.dragging = true); const dragImage = dragImageProvider(); cellRoot.parentElement!.appendChild(dragImage); event.dataTransfer.setDragImage(dragImage, 0, 0); setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout - - container.classList.add(DRAGGING_CLASS); }; for (const dragHandle of dragHandles) { templateData.templateDisposables.add(DOM.addDisposableListener(dragHandle, DOM.EventType.DRAG_START, onDragStart)); @@ -423,3 +407,88 @@ export class CellDragAndDropController extends Disposable { return this.getDropInsertDirection(dragPosRatio); } } + +export function performCellDropEdits(editor: INotebookEditorDelegate, draggedCell: ICellViewModel, dropDirection: 'above' | 'below', draggedOverCell: ICellViewModel): void { + const draggedCellIndex = editor.getCellIndex(draggedCell)!; + let originalToIdx = editor.getCellIndex(draggedOverCell)!; + + if (typeof draggedCellIndex !== 'number' || typeof originalToIdx !== 'number') { + return; + } + + // If dropped on a folded markdown range, insert after the folding range + if (dropDirection === 'below') { + const newIdx = editor.getNextVisibleCellIndex(originalToIdx) ?? originalToIdx; + originalToIdx = newIdx; + } + + let selections = editor.getSelections(); + if (!selections.length) { + selections = [editor.getFocus()]; + } + + const droppedInSelection = selections.find(range => range.start <= originalToIdx && range.end > originalToIdx); + if (droppedInSelection) { + originalToIdx = droppedInSelection.start; + } + + const originalFocusIdx = editor.getFocus().start; + let numCells = 0; + let focusNewIdx = originalToIdx; + let newInsertionIdx = originalToIdx; + + // Compute a set of edits which will be applied in reverse order by the notebook text model. + // `index`: the starting index of the range, after previous edits have been applied + // `newIdx`: the destination index, after this edit's range has been removed + selections.sort((a, b) => b.start - a.start); + const edits = selections.map(range => { + const length = range.end - range.start; + + // If this range is before the insertion point, subtract the cells in this range from the "to" index + let toIndexDelta = 0; + if (range.end <= newInsertionIdx) { + toIndexDelta = -length; + } + + const newIdx = newInsertionIdx + toIndexDelta; + + // If this range contains the focused cell, set the new focus index to the new index of the cell + if (originalFocusIdx >= range.start && originalFocusIdx <= range.end) { + const offset = originalFocusIdx - range.start; + focusNewIdx = newIdx + offset; + } + + // If below the insertion point, the original index will have been shifted down + const fromIndexDelta = range.start >= originalToIdx ? numCells : 0; + + const edit: ICellMoveEdit = { + editType: CellEditType.Move, + index: range.start + fromIndexDelta, + length, + newIdx + }; + numCells += length; + + // If a range was moved down, the insertion index needs to be adjusted + if (range.end < newInsertionIdx) { + newInsertionIdx -= length; + } + + return edit; + }); + + const lastEdit = edits[edits.length - 1]; + const finalSelection = { start: lastEdit.newIdx, end: lastEdit.newIdx + numCells }; + const finalFocus = { start: focusNewIdx, end: focusNewIdx + 1 }; + + // console.log(JSON.stringify(edits)); + // console.log(JSON.stringify(finalSelection)); + // console.log(JSON.stringify(finalFocus)); + editor.textModel!.applyEdits( + edits, + true, + { kind: SelectionStateType.Index, focus: editor.getFocus(), selections: editor.getSelections() }, + () => ({ kind: SelectionStateType.Index, focus: finalFocus, selections: [finalSelection] }), + undefined); + editor.revealCellRangeInView(finalSelection); +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts new file mode 100644 index 00000000000..46388b581b4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Color } from 'vs/base/common/color'; +import * as platform from 'vs/base/common/platform'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Range } from 'vs/editor/common/core/range'; +import * as languages from 'vs/editor/common/languages'; +import { tokenizeLineToHTML } from 'vs/editor/common/languages/textToHtmlTokenizer'; +import { ITextModel } from 'vs/editor/common/model'; +import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; + +class EditorTextRenderer { + + private static _ttPolicy = window.trustedTypes?.createPolicy('cellRendererEditorText', { + createHTML(input) { return input; } + }); + + getRichText(editor: ICodeEditor, modelRange: Range): HTMLElement | null { + const model = editor.getModel(); + if (!model) { + return null; + } + + const colorMap = this.getDefaultColorMap(); + const fontInfo = editor.getOptions().get(EditorOption.fontInfo); + const fontFamilyVar = '--notebook-editor-font-family'; + const fontSizeVar = '--notebook-editor-font-size'; + const fontWeightVar = '--notebook-editor-font-weight'; + + const style = `` + + `color: ${colorMap[languages.ColorId.DefaultForeground]};` + + `background-color: ${colorMap[languages.ColorId.DefaultBackground]};` + + `font-family: var(${fontFamilyVar});` + + `font-weight: var(${fontWeightVar});` + + `font-size: var(${fontSizeVar});` + + `line-height: ${fontInfo.lineHeight}px;` + + `white-space: pre;`; + + const element = DOM.$('div', { style }); + + const fontSize = fontInfo.fontSize; + const fontWeight = fontInfo.fontWeight; + element.style.setProperty(fontFamilyVar, fontInfo.fontFamily); + element.style.setProperty(fontSizeVar, `${fontSize}px`); + element.style.setProperty(fontWeightVar, fontWeight); + + const linesHtml = this.getRichTextLinesAsHtml(model, modelRange, colorMap); + element.innerHTML = linesHtml as string; + return element; + } + + private getRichTextLinesAsHtml(model: ITextModel, modelRange: Range, colorMap: string[]): string | TrustedHTML { + const startLineNumber = modelRange.startLineNumber; + const startColumn = modelRange.startColumn; + const endLineNumber = modelRange.endLineNumber; + const endColumn = modelRange.endColumn; + + const tabSize = model.getOptions().tabSize; + + let result = ''; + + for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { + const lineTokens = model.getLineTokens(lineNumber); + const lineContent = lineTokens.getLineContent(); + const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); + const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); + + if (lineContent === '') { + result += '
'; + } else { + result += tokenizeLineToHTML(lineContent, lineTokens.inflate(), colorMap, startOffset, endOffset, tabSize, platform.isWindows); + } + } + + return EditorTextRenderer._ttPolicy?.createHTML(result) ?? result; + } + + private getDefaultColorMap(): string[] { + const colorMap = languages.TokenizationRegistry.getColorMap(); + const result: string[] = ['#000000']; + if (colorMap) { + for (let i = 1, len = colorMap.length; i < len; i++) { + result[i] = Color.Format.CSS.formatHex(colorMap[i]); + } + } + return result; + } +} + +export class CodeCellDragImageRenderer { + getDragImage(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement { + let dragImage = this.getDragImageImpl(templateData, editor, type); + if (!dragImage) { + // TODO@roblourens I don't think this can happen + dragImage = document.createElement('div'); + dragImage.textContent = '1 cell'; + } + + return dragImage; + } + + private getDragImageImpl(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement | null { + const dragImageContainer = templateData.container.cloneNode(true) as HTMLElement; + dragImageContainer.classList.forEach(c => dragImageContainer.classList.remove(c)); + dragImageContainer.classList.add('cell-drag-image', 'monaco-list-row', 'focused', `${type}-cell-row`); + + const editorContainer: HTMLElement | null = dragImageContainer.querySelector('.cell-editor-container'); + if (!editorContainer) { + return null; + } + + const richEditorText = new EditorTextRenderer().getRichText(editor, new Range(1, 1, 1, 1000)); + if (!richEditorText) { + return null; + } + DOM.reset(editorContainer, richEditorText); + + return dragImageContainer; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts index 09cc47e96e8..00d68c38911 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts @@ -19,7 +19,7 @@ import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { INotebookCellToolbarActionContext, INotebookCommandContext, NotebookMultiCellAction, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOptions } from 'vs/workbench/contrib/notebook/common/notebookOptions'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; @@ -88,19 +88,7 @@ export class CellEditorOptions extends CellPart { this._value = this._computeEditorOptions(); } - - renderCell(element: ICellViewModel): void { - // no op - } - - prepareLayout(): void { - // nothing to read - } - updateInternalLayoutNow(element: ICellViewModel): void { - // nothing to update - } - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent) { + override updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent) { if (e.cellLineNumberChanged) { this.setLineNumbers(element.lineNumbers); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index fa55fbc4bfd..d4745c02a43 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -7,39 +7,36 @@ import * as DOM from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export class CellExecutionPart extends CellPart { - private kernelDisposables = new DisposableStore(); + private kernelDisposables = this._register(new DisposableStore()); constructor( private readonly _notebookEditor: INotebookEditorDelegate, private readonly _executionOrderLabel: HTMLElement ) { super(); - } - setup(templateData: BaseCellRenderTemplate): void { this._register(this._notebookEditor.onDidChangeActiveKernel(() => { - if (templateData.currentRenderedCell) { + if (this.currentCell) { this.kernelDisposables.clear(); if (this._notebookEditor.activeKernel) { this.kernelDisposables.add(this._notebookEditor.activeKernel.onDidChange(() => { - if (templateData.currentRenderedCell) { - this.updateExecutionOrder(templateData.currentRenderedCell.internalMetadata); + if (this.currentCell) { + this.updateExecutionOrder(this.currentCell.internalMetadata); } })); } - this.updateExecutionOrder(templateData.currentRenderedCell.internalMetadata); + this.updateExecutionOrder(this.currentCell.internalMetadata); } })); } - renderCell(element: ICellViewModel, _templateData: BaseCellRenderTemplate): void { + protected override didRenderCell(element: ICellViewModel): void { this.updateExecutionOrder(element.internalMetadata); } @@ -54,13 +51,13 @@ export class CellExecutionPart extends CellPart { } } - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { + override updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { if (e.internalMetadataChanged) { this.updateExecutionOrder(element.internalMetadata); } } - updateInternalLayoutNow(element: ICellViewModel): void { + override updateInternalLayoutNow(element: ICellViewModel): void { if (element.isInputCollapsed) { DOM.hide(this._executionOrderLabel); } else { @@ -68,6 +65,4 @@ export class CellExecutionPart extends CellPart { this._executionOrderLabel.style.top = `${element.layoutInfo.editorHeight}px`; } } - - prepareLayout(): void { } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocus.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocus.ts new file mode 100644 index 00000000000..281116d7a00 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocus.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; + +export class CellFocusPart extends CellPart { + constructor( + containerElement: HTMLElement, + focusSinkElement: HTMLElement | undefined, + notebookEditor: INotebookEditor + ) { + super(); + + this._register(DOM.addDisposableListener(containerElement, DOM.EventType.FOCUS, () => { + if (this.currentCell) { + notebookEditor.focusElement(this.currentCell); + } + }, true)); + + if (focusSinkElement) { + this._register(DOM.addDisposableListener(focusSinkElement, DOM.EventType.FOCUS, () => { + if (this.currentCell && (this.currentCell as CodeCellViewModel).outputsViewModels.length) { + notebookEditor.focusNotebookCell(this.currentCell, 'output'); + } + })); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index 24f198956ad..b1ba486ec57 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -5,9 +5,9 @@ import * as DOM from 'vs/base/browser/dom'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; -import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CodeCellLayoutInfo, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CellTitleToolbarPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -16,10 +16,9 @@ export class CellFocusIndicator extends CellPart { public codeFocusIndicator: FastDomNode; public outputFocusIndicator: FastDomNode; - private currentElement: ICellViewModel | undefined; - constructor( readonly notebookEditor: INotebookEditorDelegate, + readonly titleToolbar: CellTitleToolbarPart, readonly top: FastDomNode, readonly left: FastDomNode, readonly right: FastDomNode, @@ -42,26 +41,35 @@ export class CellFocusIndicator extends CellPart { DOM.$('.codeOutput-focus-indicator.output-focus-indicator')))); this._register(DOM.addDisposableListener(this.codeFocusIndicator.domNode, DOM.EventType.CLICK, () => { - if (this.currentElement) { - this.currentElement.isInputCollapsed = !this.currentElement.isInputCollapsed; + if (this.currentCell) { + this.currentCell.isInputCollapsed = !this.currentCell.isInputCollapsed; } })); this._register(DOM.addDisposableListener(this.outputFocusIndicator.domNode, DOM.EventType.CLICK, () => { - if (this.currentElement) { - this.currentElement.isOutputCollapsed = !this.currentElement.isOutputCollapsed; + if (this.currentCell) { + this.currentCell.isOutputCollapsed = !this.currentCell.isOutputCollapsed; } })); + + this._register(DOM.addDisposableListener(this.left.domNode, DOM.EventType.DBLCLICK, e => { + if (!this.currentCell || !this.notebookEditor.hasModel()) { + return; + } + + const clickedOnInput = e.offsetY < (this.currentCell.layoutInfo as CodeCellLayoutInfo).outputContainerOffset; + if (clickedOnInput) { + this.currentCell.isInputCollapsed = !this.currentCell.isInputCollapsed; + } else { + this.currentCell.isOutputCollapsed = !this.currentCell.isOutputCollapsed; + } + })); + + this._register(this.titleToolbar.onDidUpdateActions(() => { + this.updateFocusIndicatorsForTitleMenu(); + })); } - renderCell(element: ICellViewModel): void { - this.currentElement = element; - } - - prepareLayout(): void { - // nothing to read - } - - updateInternalLayoutNow(element: ICellViewModel): void { + override updateInternalLayoutNow(element: ICellViewModel): void { if (element.cellKind === CellKind.Markup) { // markdown cell const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, (element as MarkupCellViewModel).layoutInfo.foldHintHeight, this.notebookEditor.textModel?.viewType); @@ -74,16 +82,25 @@ export class CellFocusIndicator extends CellPart { const cell = element as CodeCellViewModel; const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); const bottomToolbarDimensions = this.notebookEditor.notebookOptions.computeBottomToolbarDimensions(this.notebookEditor.textModel?.viewType); - const indicatorHeight = cell.layoutInfo.codeIndicatorHeight + cell.layoutInfo.outputIndicatorHeight; + const indicatorHeight = cell.layoutInfo.codeIndicatorHeight + cell.layoutInfo.outputIndicatorHeight + cell.layoutInfo.commentHeight; this.left.setHeight(indicatorHeight); this.right.setHeight(indicatorHeight); this.codeFocusIndicator.setHeight(cell.layoutInfo.codeIndicatorHeight); this.outputFocusIndicator.setHeight(Math.max(cell.layoutInfo.outputIndicatorHeight - cell.viewContext.notebookOptions.getLayoutConfiguration().focusIndicatorGap, 0)); this.bottom.domNode.style.transform = `translateY(${cell.layoutInfo.totalHeight - bottomToolbarDimensions.bottomToolbarGap - layoutInfo.cellBottomMargin}px)`; } + + this.updateFocusIndicatorsForTitleMenu(); } - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // nothing to update + private updateFocusIndicatorsForTitleMenu(): void { + const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); + if (this.titleToolbar.hasActions) { + this.left.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; + this.right.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; + } else { + this.left.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; + this.right.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; + } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 2dde9de4e17..7306836ac30 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -26,6 +26,7 @@ import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditorDelegate, JUPYTER_EXTENSION_ID, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { mimetypeIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -33,8 +34,6 @@ import { CellUri, IOrderedMimeType, NotebookCellOutputsSplice, RENDERER_NOT_AVAI import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -124,32 +123,43 @@ export class CellOutputElement extends Disposable { } updateOutputData() { - // update the content inside the domNode, do not need to worry about streaming - if (!this.innerContainer) { - if (this.renderResult) { - return; - } else { - // init rendering didn't happen - const currOutputIndex = this.cellOutputContainer.renderedOutputEntries.findIndex(entry => entry.element === this); - const previousSibling = currOutputIndex > 0 && !!(this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer?.parentElement) - ? this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer - : undefined; - this.render(previousSibling); - this._relayoutCell(); + if ( + this.notebookEditor.hasModel() && + this.innerContainer && + this.renderResult && + this.renderResult.type === RenderOutputType.Extension + ) { + // Output rendered by extension renderer got an update + const [mimeTypes, pick] = this.output.resolveMimeTypes(this.notebookEditor.textModel, this.notebookEditor.activeKernel?.preloadProvides); + const pickedMimeType = mimeTypes[pick]; + if (pickedMimeType.mimeType === this.renderResult.mimeType && pickedMimeType.rendererId === this.renderResult.renderer.id) { + // Same mimetype, same renderer, call the extension renderer to update + const index = this.viewCell.outputsViewModels.indexOf(this.output); + this.notebookEditor.updateOutput(this.viewCell, this.renderResult, this.viewCell.getOutputOffset(index)); return; } } - // user chooses another mimetype - const nextElement = this.innerContainer.nextElementSibling; - this._renderDisposableStore.clear(); - const element = this.innerContainer; - if (element) { - element.parentElement?.removeChild(element); - this.notebookEditor.removeInset(this.output); + if (!this.innerContainer) { + // init rendering didn't happen + const currOutputIndex = this.cellOutputContainer.renderedOutputEntries.findIndex(entry => entry.element === this); + const previousSibling = currOutputIndex > 0 && !!(this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer?.parentElement) + ? this.cellOutputContainer.renderedOutputEntries[currOutputIndex - 1].element.innerContainer + : undefined; + this.render(previousSibling); + } else { + // Another mimetype or renderer is picked, we need to clear the current output and re-render + const nextElement = this.innerContainer.nextElementSibling; + this._renderDisposableStore.clear(); + const element = this.innerContainer; + if (element) { + element.parentElement?.removeChild(element); + this.notebookEditor.removeInset(this.output); + } + + this.render(nextElement as HTMLElement); } - this.render(nextElement as HTMLElement); this._relayoutCell(); } @@ -446,11 +456,7 @@ export class CellOutputContainer extends CellPart { })); } - renderCell(element: ICellViewModel): void { - // no op - } - - updateInternalLayoutNow(viewCell: CodeCellViewModel) { + override updateInternalLayoutNow(viewCell: CodeCellViewModel) { this.templateData.outputContainer.setTop(viewCell.layoutInfo.outputContainerOffset); this.templateData.outputShowMoreContainer.setTop(viewCell.layoutInfo.outputShowMoreContainerOffset); @@ -463,14 +469,6 @@ export class CellOutputContainer extends CellPart { }); } - prepareLayout() { - } - - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // nothing to update - } - render(editorHeight: number) { if (this.viewCell.outputsViewModels.length > 0) { if (this.viewCell.layoutInfo.totalHeight !== 0 && this.viewCell.layoutInfo.editorHeight > editorHeight) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts index 3c51f3d1fb5..762b52945f1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts @@ -6,7 +6,7 @@ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellExecutionStateChangedEvent, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; @@ -27,23 +27,15 @@ export class CellProgressBar extends CellPart { this._collapsedProgressBar.hide(); } - renderCell(element: ICellViewModel): void { + override didRenderCell(element: ICellViewModel): void { this._updateForExecutionState(element); } - prepareLayout(): void { - // nothing to read - } - - updateInternalLayoutNow(element: ICellViewModel): void { - // nothing to update - } - override updateForExecutionState(element: ICellViewModel, e: ICellExecutionStateChangedEvent): void { this._updateForExecutionState(element, e); } - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { + override updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { if (e.metadataChanged || e.internalMetadataChanged) { this._updateForExecutionState(element); } @@ -53,12 +45,12 @@ export class CellProgressBar extends CellPart { if (element.isInputCollapsed) { this._progressBar.hide(); if (exeState?.state === NotebookCellExecutionState.Executing) { - showProgressBar(this._collapsedProgressBar); + this._updateForExecutionState(element); } } else { this._collapsedProgressBar.hide(); if (exeState?.state === NotebookCellExecutionState.Executing) { - showProgressBar(this._progressBar); + this._updateForExecutionState(element); } } } @@ -67,7 +59,7 @@ export class CellProgressBar extends CellPart { private _updateForExecutionState(element: ICellViewModel, e?: ICellExecutionStateChangedEvent): void { const exeState = e?.changed ?? this._notebookExecutionStateService.getCellExecution(element.uri); const progressBar = element.isInputCollapsed ? this._collapsedProgressBar : this._progressBar; - if (exeState?.state === NotebookCellExecutionState.Executing && !exeState.isPaused) { + if (element.isInputCollapsed || (exeState?.state === NotebookCellExecutionState.Executing && !exeState.didPause)) { showProgressBar(progressBar); } else { progressBar.hide(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index 168ee9033b9..9c636037738 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -13,6 +13,7 @@ import { stripIcons } from 'vs/base/common/iconLabels'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,11 +21,10 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, ThemeColor } from 'vs/platform/theme/common/themeService'; import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { ClickTargetType, IClickTarget } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellWidgets'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const $ = DOM.$; @@ -49,6 +49,7 @@ export class CellEditorStatusBar extends CellPart { private readonly _notebookEditor: INotebookEditorDelegate, private readonly _cellContainer: HTMLElement, editorPart: HTMLElement, + private readonly _editor: ICodeEditor | undefined, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IThemeService private readonly _themeService: IThemeService, ) { @@ -89,20 +90,51 @@ export class CellEditorStatusBar extends CellPart { } - renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { + override didRenderCell(element: ICellViewModel): void { this.updateContext({ ui: true, cell: element, notebookEditor: this._notebookEditor, $mid: MarshalledId.NotebookCellActionContext }); + + if (this._editor) { + // Focus Mode + const updateFocusModeForEditorEvent = () => { + element.focusMode = + this._editor && (this._editor.hasWidgetFocus() || (document.activeElement && this.statusBarContainer.contains(document.activeElement))) + ? CellFocusMode.Editor + : CellFocusMode.Container; + }; + + this.cellDisposables.add(this._editor.onDidFocusEditorWidget(() => { + updateFocusModeForEditorEvent(); + })); + this.cellDisposables.add(this._editor.onDidBlurEditorWidget(() => { + // this is for a special case: + // users click the status bar empty space, which we will then focus the editor + // so we don't want to update the focus state too eagerly, it will be updated with onDidFocusEditorWidget + if ( + this._notebookEditor.hasEditorFocus() && + !(document.activeElement && this.statusBarContainer.contains(document.activeElement))) { + updateFocusModeForEditorEvent(); + } + })); + + // Mouse click handlers + this.cellDisposables.add(this.onDidClick(e => { + if (this.currentCell instanceof CodeCellViewModel && e.type !== ClickTargetType.ContributedCommandItem && this._editor) { + const target = this._editor.getTargetAtClientPoint(e.event.clientX, e.event.clientY - this._notebookEditor.notebookOptions.computeEditorStatusbarHeight(this.currentCell.internalMetadata, this.currentCell.uri)); + if (target?.position) { + this._editor.setPosition(target.position); + this._editor.focus(); + } + } + })); + } } - prepareLayout(): void { - // nothing to read - } - - updateInternalLayoutNow(element: ICellViewModel): void { + override updateInternalLayoutNow(element: ICellViewModel): void { // todo@rebornix layer breaker this._cellContainer.classList.toggle('cell-statusbar-hidden', this._notebookEditor.notebookOptions.computeEditorStatusbarHeight(element.internalMetadata, element.uri) === 0); @@ -120,11 +152,6 @@ export class CellEditorStatusBar extends CellPart { this.rightItems.forEach(item => item.maxWidth = maxItemWidth); } - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // nothing to update - } - private getMaxItemWidth() { return this.width / 2; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts index 009510a82ae..45fc3cbb235 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars.ts @@ -20,11 +20,9 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { DeleteCellAction } from 'vs/workbench/contrib/notebook/browser/controller/editActions'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { registerStickyScroll } from 'vs/workbench/contrib/notebook/browser/view/cellParts/stickyScroll'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; export class BetweenCellToolbar extends CellPart { private _betweenCellToolbar!: ToolBar; @@ -73,29 +71,19 @@ export class BetweenCellToolbar extends CellPart { this._betweenCellToolbar.context = context; } - renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { + override didRenderCell(element: ICellViewModel): void { this._betweenCellToolbar.context = { ui: true, cell: element, - cellTemplate: templateData, notebookEditor: this._notebookEditor, $mid: MarshalledId.NotebookCellActionContext }; } - prepareLayout(): void { - // nothing to read - } - - updateInternalLayoutNow(element: ICellViewModel) { + override updateInternalLayoutNow(element: ICellViewModel) { const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset; this._bottomCellToolbarContainer.style.transform = `translateY(${bottomToolbarOffset}px)`; } - - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // nothing to update - } } @@ -113,8 +101,6 @@ export class CellTitleToolbarPart extends CellPart { private readonly _onDidUpdateActions: Emitter = this._register(new Emitter()); readonly onDidUpdateActions: Event = this._onDidUpdateActions.event; - private cellDisposable = this._register(new DisposableStore()); - get hasActions(): boolean { return this._hasActions; } @@ -141,9 +127,8 @@ export class CellTitleToolbarPart extends CellPart { this.setupChangeListeners(); } - renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { - this.cellDisposable.clear(); - this.cellDisposable.add(registerStickyScroll(this._notebookEditor, element, this.toolbarContainer, { extraOffset: 4, min: -14 })); + override didRenderCell(element: ICellViewModel): void { + this.cellDisposables.add(registerStickyScroll(this._notebookEditor, element, this.toolbarContainer, { extraOffset: 4, min: -14 })); this.updateContext({ ui: true, @@ -153,16 +138,6 @@ export class CellTitleToolbarPart extends CellPart { }); } - prepareLayout(): void { - // nothing to read - } - updateInternalLayoutNow(element: ICellViewModel): void { - // no op - } - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // no op - } - private updateContext(toolbarContext: INotebookCellActionContext) { this._toolbar.context = toolbarContext; this._deleteToolbar.context = toolbarContext; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 1c41fc3962a..5dfd489841c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -22,15 +22,13 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions'; import { CellOutputContainer } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; -import { ClickTargetType } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellWidgets'; -import { CodeCellExecutionIcon } from 'vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; +import { CollapsedCodeCellExecutionIcon } from 'vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon'; import { CodeCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; - export class CodeCell extends Disposable { private _outputContainerRenderer: CellOutputContainer; @@ -39,13 +37,12 @@ export class CodeCell extends Disposable { private _isDisposed: boolean = false; private readonly cellParts: CellPart[]; - private _collapsedExecutionIcon: CodeCellExecutionIcon; + private _collapsedExecutionIcon: CollapsedCodeCellExecutionIcon; constructor( private readonly notebookEditor: IActiveNotebookEditorDelegate, private readonly viewCell: CodeCellViewModel, private readonly templateData: CodeCellRenderTemplate, - cellPartTemplates: CellPart[], @IInstantiationService private readonly instantiationService: IInstantiationService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @IKeybindingService readonly keybindingService: IKeybindingService, @@ -57,8 +54,8 @@ export class CodeCell extends Disposable { super(); const cellEditorOptions = this._register(new CellEditorOptions(this.notebookEditor, this.notebookEditor.notebookOptions, this.configurationService, this.viewCell.language)); - this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 500 }); - this.cellParts = [...cellPartTemplates, cellEditorOptions, this._outputContainerRenderer]; + this._outputContainerRenderer = this.instantiationService.createInstance(CellOutputContainer, notebookEditor, viewCell, templateData, { limit: 2 }); + this.cellParts = [...templateData.cellParts, cellEditorOptions, this._outputContainerRenderer]; const editorHeight = this.calculateInitEditorHeight(); this.initializeEditor(editorHeight); @@ -107,9 +104,10 @@ export class CodeCell extends Disposable { } })); - this.cellParts.forEach(cellPart => { - cellPart.renderCell(this.viewCell, this.templateData); - }); + this.cellParts.forEach(cellPart => cellPart.renderCell(this.viewCell)); + this._register(toDisposable(() => { + this.cellParts.forEach(cellPart => cellPart.unrenderCell(this.viewCell)); + })); this.updateEditorOptions(); this.updateEditorForFocusModeChange(); @@ -132,7 +130,7 @@ export class CodeCell extends Disposable { this._register(toDisposable(() => { executionItemElement.parentElement?.removeChild(executionItemElement); })); - this._collapsedExecutionIcon = this.instantiationService.createInstance(CodeCellExecutionIcon, this.notebookEditor, this.viewCell, executionItemElement); + this._collapsedExecutionIcon = this.instantiationService.createInstance(CollapsedCodeCellExecutionIcon, this.notebookEditor, this.viewCell, executionItemElement); this.updateForCollapseState(); this._register(Event.runAndSubscribe(viewCell.onDidChangeOutputs, this.updateForOutputs.bind(this))); @@ -271,28 +269,6 @@ export class CodeCell extends Disposable { this.notebookEditor.revealLineInViewAsync(this.viewCell, lastSelection.positionLineNumber); } })); - - // Focus Mode - const updateFocusModeForEditorEvent = () => { - this.viewCell.focusMode = - (this.templateData.editor.hasWidgetFocus() || (document.activeElement && this.templateData.statusBar.statusBarContainer.contains(document.activeElement))) - ? CellFocusMode.Editor - : CellFocusMode.Container; - }; - - this._register(this.templateData.editor.onDidFocusEditorWidget(() => { - updateFocusModeForEditorEvent(); - })); - this._register(this.templateData.editor.onDidBlurEditorWidget(() => { - // this is for a special case: - // users click the status bar empty space, which we will then focus the editor - // so we don't want to update the focus state too eagerly, it will be updated with onDidFocusEditorWidget - if ( - this.notebookEditor.hasEditorFocus() && - !(document.activeElement && this.templateData.statusBar.statusBarContainer.contains(document.activeElement))) { - updateFocusModeForEditorEvent(); - } - })); } private registerDecorations() { @@ -331,17 +307,6 @@ export class CodeCell extends Disposable { } private registerMouseListener() { - // Mouse click handlers - this._register(this.templateData.statusBar.onDidClick(e => { - if (e.type !== ClickTargetType.ContributedCommandItem) { - const target = this.templateData.editor.getTargetAtClientPoint(e.event.clientX, e.event.clientY - this.notebookEditor.notebookOptions.computeEditorStatusbarHeight(this.viewCell.internalMetadata, this.viewCell.uri)); - if (target?.position) { - this.templateData.editor.setPosition(target.position); - this.templateData.editor.focus(); - } - } - })); - this._register(this.templateData.editor.onMouseDown(e => { // prevent default on right mouse click, otherwise it will trigger unexpected focus changes // the catch is, it means we don't allow customization of right button mouse down handlers other than the built in ones. @@ -521,7 +486,6 @@ export class CodeCell extends Disposable { this.viewCell.detachTextEditor(); this._removeInputCollapsePreview(); this._outputContainerRenderer.dispose(); - this.templateData.focusIndicator.left.setHeight(0); this._pendingLayout?.dispose(); super.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon.ts index cd463a01580..38a21aa16c7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellExecutionIcon.ts @@ -18,7 +18,7 @@ interface IExecutionItem { tooltip?: string; } -export class CodeCellExecutionIcon extends Disposable { +export class CollapsedCodeCellExecutionIcon extends Disposable { private _visible = false; constructor( @@ -79,9 +79,7 @@ export class CodeCellExecutionIcon extends Disposable { tooltip: localize('notebook.cell.status.pending', "Pending"), }; } else if (state === NotebookCellExecutionState.Executing) { - const icon = runState?.isPaused ? - executingStateIcon : - ThemeIcon.modify(executingStateIcon, 'spin'); + const icon = ThemeIcon.modify(executingStateIcon, 'spin'); return { text: `$(${icon.id})`, tooltip: localize('notebook.cell.status.executing', "Executing"), diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts index ae6d1c831ab..3e6e41b48ed 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar.ts @@ -19,17 +19,13 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { registerStickyScroll } from 'vs/workbench/contrib/notebook/browser/view/cellParts/stickyScroll'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; export class RunToolbar extends CellPart { private toolbar!: ToolBar; - private cellDisposable = this._register(new DisposableStore()); - constructor( readonly notebookEditor: INotebookEditorDelegate, readonly contextKeyService: IContextKeyService, @@ -54,9 +50,8 @@ export class RunToolbar extends CellPart { this._register(this.notebookEditor.notebookOptions.onDidChangeOptions(updateActions)); } - renderCell(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { - this.cellDisposable.clear(); - this.cellDisposable.add(registerStickyScroll(this.notebookEditor, element, this.runButtonContainer)); + override didRenderCell(element: ICellViewModel): void { + this.cellDisposables.add(registerStickyScroll(this.notebookEditor, element, this.runButtonContainer)); this.toolbar.context = { ui: true, @@ -66,18 +61,6 @@ export class RunToolbar extends CellPart { }; } - prepareLayout(): void { - // no op - } - - updateInternalLayoutNow(element: ICellViewModel): void { - // no op - } - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // no op - } - getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { const primary: IAction[] = []; const secondary: IAction[] = []; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellInput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellInput.ts new file mode 100644 index 00000000000..4d29d44bc05 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellInput.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; + +export class CollapsedCellInput extends CellPart { + constructor( + private readonly notebookEditor: INotebookEditor, + cellInputCollapsedContainer: HTMLElement, + ) { + super(); + + this._register(DOM.addDisposableListener(cellInputCollapsedContainer, DOM.EventType.DBLCLICK, e => { + if (!this.currentCell || !this.notebookEditor.hasModel()) { + return; + } + + if (this.currentCell.isInputCollapsed) { + this.currentCell.isInputCollapsed = false; + } else { + this.currentCell.isOutputCollapsed = false; + } + })); + + this._register(DOM.addDisposableListener(cellInputCollapsedContainer, DOM.EventType.CLICK, e => { + if (!this.currentCell || !this.notebookEditor.hasModel()) { + return; + } + + const element = e.target as HTMLElement; + + if (element && element.classList && element.classList.contains('expandInputIcon')) { + // clicked on the expand icon + this.currentCell.isInputCollapsed = false; + } + })); + } +} + diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput.ts new file mode 100644 index 00000000000..560a6f009a1 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; +import { localize } from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { EXPAND_CELL_OUTPUT_COMMAND_ID, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; + +const $ = DOM.$; + +export class CollapsedCellOutput extends CellPart { + constructor( + private readonly notebookEditor: INotebookEditor, + cellOutputCollapseContainer: HTMLElement, + @IKeybindingService keybindingService: IKeybindingService + ) { + super(); + + const placeholder = DOM.append(cellOutputCollapseContainer, $('span.expandOutputPlaceholder')) as HTMLElement; + placeholder.textContent = localize('cellOutputsCollapsedMsg', "Outputs are collapsed"); + const expandIcon = DOM.append(cellOutputCollapseContainer, $('span.expandOutputIcon')); + expandIcon.classList.add(...CSSIcon.asClassNameArray(Codicon.more)); + + const keybinding = keybindingService.lookupKeybinding(EXPAND_CELL_OUTPUT_COMMAND_ID); + if (keybinding) { + placeholder.title = localize('cellExpandOutputButtonLabelWithDoubleClick', "Double click to expand cell output ({0})", keybinding.getLabel()); + cellOutputCollapseContainer.title = localize('cellExpandOutputButtonLabel', "Expand Cell Output (${0})", keybinding.getLabel()); + } + + DOM.hide(cellOutputCollapseContainer); + + this._register(DOM.addDisposableListener(expandIcon, DOM.EventType.CLICK, () => this.expand())); + this._register(DOM.addDisposableListener(cellOutputCollapseContainer, DOM.EventType.DBLCLICK, () => this.expand())); + } + + private expand() { + if (!this.currentCell) { + return; + } + + if (!this.currentCell) { + return; + } + + const textModel = this.notebookEditor.textModel!; + const index = textModel.cells.indexOf(this.currentCell.model); + + if (index < 0) { + return; + } + + this.currentCell.isOutputCollapsed = !this.currentCell.isOutputCollapsed; + } +} 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 72baae539fc..e8b61a303d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint.ts @@ -7,10 +7,8 @@ import * as DOM from 'vs/base/browser/dom'; import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { localize } from 'vs/nls'; import { FoldingController } from 'vs/workbench/contrib/notebook/browser/controller/foldingController'; -import { CellEditState, CellFoldingState, ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; -import { BaseCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { CellEditState, CellFoldingState, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; export class FoldedCellHint extends CellPart { @@ -22,7 +20,7 @@ export class FoldedCellHint extends CellPart { super(); } - renderCell(element: MarkupCellViewModel, templateData: BaseCellRenderTemplate): void { + override didRenderCell(element: MarkupCellViewModel): void { this.update(element); } @@ -68,15 +66,7 @@ export class FoldedCellHint extends CellPart { return expandIcon; } - prepareLayout(): void { - // nothing to read - } - - updateInternalLayoutNow(element: MarkupCellViewModel) { + override updateInternalLayoutNow(element: MarkupCellViewModel) { this.update(element); } - - updateState(element: ICellViewModel, e: CellViewModelStateChangeEvent): void { - // nothing to update - } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts index a68d73dc27e..b1b5077afea 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell.ts @@ -7,28 +7,27 @@ import * as DOM from 'vs/base/browser/dom'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { disposableTimeout, raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ILanguageService } from 'vs/editor/common/languages/language'; import { tokenizeToStringSync } from 'vs/editor/common/languages/textToHtmlTokenizer'; import { IReadonlyTextBuffer } from 'vs/editor/common/model'; -import { ILanguageService } from 'vs/editor/common/languages/language'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CellEditState, CellFocusMode, CellFoldingState, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditorDelegate, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellPart'; import { MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { localize } from 'vs/nls'; -import { Codicon, CSSIcon } from 'vs/base/common/codicons'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class StatefulMarkdownCell extends Disposable { @@ -48,7 +47,6 @@ export class StatefulMarkdownCell extends Disposable { private readonly notebookEditor: IActiveNotebookEditorDelegate, private readonly viewCell: MarkupCellViewModel, private readonly templateData: MarkdownCellRenderTemplate, - private readonly cellParts: CellPart[], private readonly renderedEditors: Map, @IContextKeyService private readonly contextKeyService: IContextKeyService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @@ -70,9 +68,10 @@ export class StatefulMarkdownCell extends Disposable { this.registerListeners(); // update for init state - this.cellParts.forEach(cellPart => { - cellPart.renderCell(this.viewCell, this.templateData); - }); + this.templateData.cellParts.forEach(cellPart => cellPart.renderCell(this.viewCell)); + this._register(toDisposable(() => { + this.templateData.cellParts.forEach(cellPart => cellPart.unrenderCell(this.viewCell)); + })); this.updateForHover(); this.updateForFocusModeChange(); @@ -95,7 +94,7 @@ export class StatefulMarkdownCell extends Disposable { } layoutCellParts() { - this.cellParts.forEach(part => { + this.templateData.cellParts.forEach(part => { part.updateInternalLayoutNow(this.viewCell); }); } @@ -117,7 +116,7 @@ export class StatefulMarkdownCell extends Disposable { private registerListeners() { this._register(this.viewCell.onDidChangeState(e => { - this.cellParts.forEach(cellPart => { + this.templateData.cellParts.forEach(cellPart => { cellPart.updateState(this.viewCell, e); }); })); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index f243383ea9f..673fe4a4312 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -8,7 +8,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list'; import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { isMacintosh } from 'vs/base/common/platform'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { Range } from 'vs/editor/common/core/range'; @@ -89,6 +89,10 @@ export class NotebookCellList extends WorkbenchList implements ID get rowsContainer(): HTMLElement { return this.view.containerDomNode; } + + get scrollableElement(): HTMLElement { + return this.view.scrollableElementDomNode; + } private _previousFocusedElements: CellViewModel[] = []; private readonly _localDisposableStore = new DisposableStore(); private readonly _viewModelStore = new DisposableStore(); @@ -1410,6 +1414,10 @@ export class NotebookCellList extends WorkbenchList implements ID return this.view.renderHeight; } + getScrollHeight() { + return this.view.scrollHeight; + } + override layout(height?: number, width?: number): void { this._isInLayout = true; super.layout(height, width); @@ -1436,6 +1444,147 @@ export class NotebookCellList extends WorkbenchList implements ID } } + +export class ListViewInfoAccessor extends Disposable { + constructor( + readonly list: INotebookCellList + ) { + super(); + } + + setScrollTop(scrollTop: number) { + this.list.scrollTop = scrollTop; + } + + isScrolledToBottom() { + return this.list.isScrolledToBottom(); + } + + scrollToBottom() { + this.list.scrollToBottom(); + } + + revealCellRangeInView(range: ICellRange) { + return this.list.revealElementsInView(range); + } + + revealInView(cell: ICellViewModel) { + this.list.revealElementInView(cell); + } + + revealInViewAtTop(cell: ICellViewModel) { + this.list.revealElementInViewAtTop(cell); + } + + revealInCenterIfOutsideViewport(cell: ICellViewModel) { + this.list.revealElementInCenterIfOutsideViewport(cell); + } + + async revealInCenterIfOutsideViewportAsync(cell: ICellViewModel) { + return this.list.revealElementInCenterIfOutsideViewportAsync(cell); + } + + revealInCenter(cell: ICellViewModel) { + this.list.revealElementInCenter(cell); + } + + async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { + return this.list.revealElementLineInViewAsync(cell, line); + } + + async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise { + return this.list.revealElementLineInCenterAsync(cell, line); + } + + async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise { + return this.list.revealElementLineInCenterIfOutsideViewportAsync(cell, line); + } + + async revealRangeInViewAsync(cell: ICellViewModel, range: Range): Promise { + return this.list.revealElementRangeInViewAsync(cell, range); + } + + async revealRangeInCenterAsync(cell: ICellViewModel, range: Range): Promise { + return this.list.revealElementRangeInCenterAsync(cell, range); + } + + async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise { + return this.list.revealElementRangeInCenterIfOutsideViewportAsync(cell, range); + } + + async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise { + return this.list.revealElementOffsetInCenterAsync(cell, offset); + } + + getViewIndex(cell: ICellViewModel): number { + return this.list.getViewIndex(cell) ?? -1; + } + + getViewHeight(cell: ICellViewModel): number { + if (!this.list.viewModel) { + return -1; + } + + return this.list.elementHeight(cell); + } + + getCellRangeFromViewRange(startIndex: number, endIndex: number): ICellRange | undefined { + if (!this.list.viewModel) { + return undefined; + } + + const modelIndex = this.list.getModelIndex2(startIndex); + if (modelIndex === undefined) { + throw new Error(`startIndex ${startIndex} out of boundary`); + } + + if (endIndex >= this.list.length) { + // it's the end + const endModelIndex = this.list.viewModel.length; + return { start: modelIndex, end: endModelIndex }; + } else { + const endModelIndex = this.list.getModelIndex2(endIndex); + if (endModelIndex === undefined) { + throw new Error(`endIndex ${endIndex} out of boundary`); + } + return { start: modelIndex, end: endModelIndex }; + } + } + + getCellsFromViewRange(startIndex: number, endIndex: number): ReadonlyArray { + if (!this.list.viewModel) { + return []; + } + + const range = this.getCellRangeFromViewRange(startIndex, endIndex); + if (!range) { + return []; + } + + return this.list.viewModel.getCellsInRange(range); + } + + getCellsInRange(range?: ICellRange): ReadonlyArray { + return this.list.viewModel?.getCellsInRange(range) ?? []; + } + + setCellEditorSelection(cell: ICellViewModel, range: Range): void { + this.list.setCellSelection(cell, range); + } + + setHiddenAreas(_ranges: ICellRange[]): boolean { + return this.list.setHiddenAreas(_ranges, true); + } + + getVisibleRangesPlusViewportBelow(): ICellRange[] { + return this.list?.getVisibleRangesPlusViewportBelow() ?? []; + } + + triggerScroll(event: IMouseWheelEvent) { + this.list.triggerScrollFromMouseWheelEvent(event); + } +} + function getEditorAttachedPromise(element: CellViewModel) { return new Promise((resolve, reject) => { Event.once(element.onDidChangeEditorAttachState)(() => element.editorAttached ? resolve() : reject()); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index a1def1d770c..0eb419e6d32 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -15,13 +15,7 @@ import { Range } from 'vs/editor/common/core/range'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellExecutionPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution'; -import { CellFocusIndicator } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator'; -import { CellProgressBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar'; -import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart'; -import { BetweenCellToolbar, CellTitleToolbarPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars'; -import { RunToolbar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar'; -import { FoldedCellHint } from 'vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -44,6 +38,7 @@ export interface INotebookCellList { scrollLeft: number; length: number; rowsContainer: HTMLElement; + scrollableElement: HTMLElement; readonly onDidRemoveOutputs: Event; readonly onDidHideOutputs: Event; readonly onDidRemoveCellsFromView: Event; @@ -89,6 +84,7 @@ export interface INotebookCellList { setCellSelection(element: ICellViewModel, range: Range): void; style(styles: IListStyles): void; getRenderHeight(): number; + getScrollHeight(): number; updateOptions(options: IListOptions): void; layout(height?: number, width?: number): void; dispose(): void; @@ -101,31 +97,23 @@ export interface BaseCellRenderTemplate { instantiationService: IInstantiationService; container: HTMLElement; cellContainer: HTMLElement; - decorationContainer: HTMLElement; - betweenCellToolbar: BetweenCellToolbar; - titleToolbar: CellTitleToolbarPart; - focusIndicator: CellFocusIndicator; readonly templateDisposables: DisposableStore; readonly elementDisposables: DisposableStore; currentRenderedCell?: ICellViewModel; - statusBar: CellEditorStatusBar; + cellParts: CellPart[]; toJSON: () => object; } export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { editorContainer: HTMLElement; foldingIndicator: HTMLElement; - foldedCellHint: FoldedCellHint; currentEditor?: ICodeEditor; } export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { - runToolbar: RunToolbar; outputContainer: FastDomNode; cellOutputCollapsedContainer: HTMLElement; outputShowMoreContainer: FastDomNode; focusSinkElement: HTMLElement; editor: ICodeEditor; - progressBar: CellProgressBar; - cellExecution: CellExecutionPart; } 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 aa3313aa51e..7bda661ed4e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -32,6 +32,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { asWebviewUri, webviewGenericCspSource } from 'vs/workbench/common/webview'; import { CellEditState, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IDisplayOutputViewModel, IFocusNotebookCellOptions, IGenericCellViewModel, IInsetRenderOutput, INotebookEditorCreationOptions, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_WEBVIEW_BOUNDARY } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads'; import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; @@ -42,7 +43,7 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { IWebviewElement, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, ToWebviewMessage } from './webviewMessages'; +import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, ToWebviewMessage } from './webviewMessages'; export interface ICachedInset { outputId: string; @@ -87,6 +88,7 @@ interface BacklayerWebviewOptions { readonly runGutter: number; readonly dragAndDropEnabled: boolean; readonly fontSize: number; + readonly fontFamily: string; readonly markupFontSize: number; } @@ -203,6 +205,7 @@ export class BackLayerWebView extends Disposable { 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, 'notebook-cell-output-font-size': `${this.options.fontSize}px`, + 'notebook-cell-output-font-family': this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ key: 'notebook.error.rendererNotFound', @@ -561,273 +564,250 @@ var requirejs = (function() { } switch (data.type) { - case 'initialized': + case 'initialized': { this.initializeWebViewState(); break; - case 'dimension': - { - for (const update of data.updates) { - const height = update.height; - if (update.isOutput) { - const resolvedResult = this.resolveOutputId(update.id); - if (resolvedResult) { - const { cellInfo, output } = resolvedResult; - this.notebookEditor.updateOutputHeight(cellInfo, output, height, !!update.init, 'webview#dimension'); - this.notebookEditor.scheduleOutputHeightAck(cellInfo, update.id, height); - } - } else { - this.notebookEditor.updateMarkupCellHeight(update.id, height, !!update.init); + } + case 'dimension': { + for (const update of data.updates) { + const height = update.height; + if (update.isOutput) { + const resolvedResult = this.resolveOutputId(update.id); + if (resolvedResult) { + const { cellInfo, output } = resolvedResult; + this.notebookEditor.updateOutputHeight(cellInfo, output, height, !!update.init, 'webview#dimension'); + this.notebookEditor.scheduleOutputHeightAck(cellInfo, update.id, height); } + } else { + this.notebookEditor.updateMarkupCellHeight(update.id, height, !!update.init); } - break; } - case 'mouseenter': - { - const resolvedResult = this.resolveOutputId(data.id); - if (resolvedResult) { - const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); - if (latestCell) { - latestCell.outputIsHovered = true; - } + break; + } + case 'mouseenter': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.outputIsHovered = true; } - break; } - case 'mouseleave': - { - const resolvedResult = this.resolveOutputId(data.id); - if (resolvedResult) { - const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); - if (latestCell) { - latestCell.outputIsHovered = false; - } + break; + } + case 'mouseleave': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.outputIsHovered = false; } - break; } - case 'outputFocus': - { - const resolvedResult = this.resolveOutputId(data.id); - if (resolvedResult) { - const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); - if (latestCell) { - latestCell.outputIsFocused = true; - } + break; + } + case 'outputFocus': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.outputIsFocused = true; } - break; } - case 'outputBlur': - { - const resolvedResult = this.resolveOutputId(data.id); - if (resolvedResult) { - const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); - if (latestCell) { - latestCell.outputIsFocused = false; - } + break; + } + case 'outputBlur': { + const resolvedResult = this.resolveOutputId(data.id); + if (resolvedResult) { + const latestCell = this.notebookEditor.getCellByInfo(resolvedResult.cellInfo); + if (latestCell) { + latestCell.outputIsFocused = false; } - break; } - case 'scroll-ack': - { - // const date = new Date(); - // const top = data.data.top; - // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); - break; - } - case 'scroll-to-reveal': - { - this.notebookEditor.setScrollTop(data.scrollTop); - break; - } - case 'did-scroll-wheel': - { - this.notebookEditor.triggerScroll({ - ...data.payload, - preventDefault: () => { }, - stopPropagation: () => { } - }); - break; - } - case 'focus-editor': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell) { - if (data.focusNext) { - this.notebookEditor.focusNextNotebookCell(cell, 'editor'); - } else { - this.notebookEditor.focusNotebookCell(cell, 'editor'); - } + break; + } + case 'scroll-ack': { + // const date = new Date(); + // const top = data.data.top; + // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); + break; + } + case 'scroll-to-reveal': { + this.notebookEditor.setScrollTop(data.scrollTop - NOTEBOOK_WEBVIEW_BOUNDARY); + break; + } + case 'did-scroll-wheel': { + this.notebookEditor.triggerScroll({ + ...data.payload, + preventDefault: () => { }, + stopPropagation: () => { } + }); + break; + } + case 'focus-editor': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell) { + if (data.focusNext) { + this.notebookEditor.focusNextNotebookCell(cell, 'editor'); + } else { + this.notebookEditor.focusNotebookCell(cell, 'editor'); } - break; } - case 'clicked-data-url': - { - this._onDidClickDataLink(data); - break; - } - case 'clicked-link': - { - let linkToOpen: URI | string | undefined; - if (matchesScheme(data.href, Schemas.command)) { - const ret = /command\:workbench\.action\.openLargeOutput\?(.*)/.exec(data.href); - if (ret && ret.length === 2) { - const outputId = ret[1]; - const group = this.editorGroupService.activeGroup; + break; + } + case 'clicked-data-url': { + this._onDidClickDataLink(data); + break; + } + case 'clicked-link': { + let linkToOpen: URI | string | undefined; + if (matchesScheme(data.href, Schemas.command)) { + const ret = /command\:workbench\.action\.openLargeOutput\?(.*)/.exec(data.href); + if (ret && ret.length === 2) { + const outputId = ret[1]; + const group = this.editorGroupService.activeGroup; - if (group) { - if (group.activeEditor) { - group.pinEditor(group.activeEditor); - } + if (group) { + if (group.activeEditor) { + group.pinEditor(group.activeEditor); } + } - this.openerService.open(CellUri.generateCellOutputUri(this.documentUri, outputId)); + this.openerService.open(CellUri.generateCellOutputUri(this.documentUri, outputId)); + return; + } + } + if (matchesSomeScheme(data.href, Schemas.http, Schemas.https, Schemas.mailto, Schemas.command, Schemas.vscodeNotebookCell, Schemas.vscodeNotebook)) { + linkToOpen = data.href; + } else if (!/^[\w\-]+:/.test(data.href)) { + if (this.documentUri.scheme === Schemas.untitled) { + const folders = this.workspaceContextService.getWorkspace().folders; + if (!folders.length) { return; } - } - if (matchesSomeScheme(data.href, Schemas.http, Schemas.https, Schemas.mailto, Schemas.command, Schemas.vscodeNotebookCell, Schemas.vscodeNotebook)) { - linkToOpen = data.href; - } else if (!/^[\w\-]+:/.test(data.href)) { - if (this.documentUri.scheme === Schemas.untitled) { - const folders = this.workspaceContextService.getWorkspace().folders; - if (!folders.length) { - return; - } - linkToOpen = URI.joinPath(folders[0].uri, data.href); - } else { - if (data.href.startsWith('/')) { - // Resolve relative to workspace - let folder = this.workspaceContextService.getWorkspaceFolder(this.documentUri); - if (!folder) { - const folders = this.workspaceContextService.getWorkspace().folders; - if (!folders.length) { - return; - } - folder = folders[0]; + linkToOpen = URI.joinPath(folders[0].uri, data.href); + } else { + if (data.href.startsWith('/')) { + // Resolve relative to workspace + let folder = this.workspaceContextService.getWorkspaceFolder(this.documentUri); + if (!folder) { + const folders = this.workspaceContextService.getWorkspace().folders; + if (!folders.length) { + return; } - linkToOpen = URI.joinPath(folder.uri, data.href); - } else { - // Resolve relative to notebook document - linkToOpen = URI.joinPath(dirname(this.documentUri), data.href); + folder = folders[0]; } - } - } - - if (linkToOpen) { - this.openerService.open(linkToOpen, { fromUserGesture: true, allowCommands: true }); - } - break; - } - case 'customKernelMessage': - { - this._onMessage.fire({ message: data.message }); - break; - } - case 'customRendererMessage': - { - this.rendererMessaging?.postMessage(data.rendererId, data.message); - break; - } - case 'clickMarkupCell': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell) { - if (data.shiftKey || (isMacintosh ? data.metaKey : data.ctrlKey)) { - // Modify selection - this.notebookEditor.toggleNotebookCellSelection(cell, /* fromPrevious */ data.shiftKey); + linkToOpen = URI.joinPath(folder.uri, data.href); } else { - // Normal click - this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); + // Resolve relative to notebook document + linkToOpen = URI.joinPath(dirname(this.documentUri), data.href); } } - break; } - case 'contextMenuMarkupCell': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell) { - // Focus the cell first + + if (linkToOpen) { + this.openerService.open(linkToOpen, { fromUserGesture: true, allowCommands: true }); + } + break; + } + case 'customKernelMessage': { + this._onMessage.fire({ message: data.message }); + break; + } + case 'customRendererMessage': { + this.rendererMessaging?.postMessage(data.rendererId, data.message); + break; + } + case 'clickMarkupCell': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell) { + if (data.shiftKey || (isMacintosh ? data.metaKey : data.ctrlKey)) { + // Modify selection + this.notebookEditor.toggleNotebookCellSelection(cell, /* fromPrevious */ data.shiftKey); + } else { + // Normal click this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); + } + } + break; + } + case 'contextMenuMarkupCell': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell) { + // Focus the cell first + this.notebookEditor.focusNotebookCell(cell, 'container', { skipReveal: true }); - // Then show the context menu - const webviewRect = this.element.getBoundingClientRect(); - this.contextMenuService.showContextMenu({ - getActions: () => { - const result: IAction[] = []; - const menu = this.menuService.createMenu(MenuId.NotebookCellTitle, this.contextKeyService); - createAndFillInContextMenuActions(menu, undefined, result); - menu.dispose(); - return result; - }, - getAnchor: () => ({ - x: webviewRect.x + data.clientX, - y: webviewRect.y + data.clientY - }) - }); - } - break; - } - case 'toggleMarkupPreview': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell && !this.notebookEditor.creationOptions.isReadOnly) { - this.notebookEditor.setMarkupCellEditState(data.cellId, CellEditState.Editing); - this.notebookEditor.focusNotebookCell(cell, 'editor', { skipReveal: true }); - } - break; - } - case 'mouseEnterMarkupCell': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell instanceof MarkupCellViewModel) { - cell.cellIsHovered = true; - } - break; - } - case 'mouseLeaveMarkupCell': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell instanceof MarkupCellViewModel) { - cell.cellIsHovered = false; - } - break; - } - case 'cell-drag-start': - { - this.notebookEditor.didStartDragMarkupCell(data.cellId, data); - break; - } - case 'cell-drag': - { - this.notebookEditor.didDragMarkupCell(data.cellId, data); - break; - } - case 'cell-drop': - { - this.notebookEditor.didDropMarkupCell(data.cellId, { - dragOffsetY: data.dragOffsetY, - ctrlKey: data.ctrlKey, - altKey: data.altKey, + // Then show the context menu + const webviewRect = this.element.getBoundingClientRect(); + this.contextMenuService.showContextMenu({ + getActions: () => { + const result: IAction[] = []; + const menu = this.menuService.createMenu(MenuId.NotebookCellTitle, this.contextKeyService); + createAndFillInContextMenuActions(menu, undefined, result); + menu.dispose(); + return result; + }, + getAnchor: () => ({ + x: webviewRect.x + data.clientX, + y: webviewRect.y + data.clientY + }) }); - break; } - case 'cell-drag-end': - { - this.notebookEditor.didEndDragMarkupCell(data.cellId); - break; + break; + } + case 'toggleMarkupPreview': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell && !this.notebookEditor.creationOptions.isReadOnly) { + this.notebookEditor.setMarkupCellEditState(data.cellId, CellEditState.Editing); + this.notebookEditor.focusNotebookCell(cell, 'editor', { skipReveal: true }); + } + break; + } + case 'mouseEnterMarkupCell': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell instanceof MarkupCellViewModel) { + cell.cellIsHovered = true; + } + break; + } + case 'mouseLeaveMarkupCell': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell instanceof MarkupCellViewModel) { + cell.cellIsHovered = false; + } + break; + } + case 'cell-drag-start': { + this.notebookEditor.didStartDragMarkupCell(data.cellId, data); + break; + } + case 'cell-drag': { + this.notebookEditor.didDragMarkupCell(data.cellId, data); + break; + } + case 'cell-drop': { + this.notebookEditor.didDropMarkupCell(data.cellId, { + dragOffsetY: data.dragOffsetY, + ctrlKey: data.ctrlKey, + altKey: data.altKey, + }); + break; + } + case 'cell-drag-end': { + this.notebookEditor.didEndDragMarkupCell(data.cellId); + break; + } + case 'renderedMarkup': { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell instanceof MarkupCellViewModel) { + cell.renderedHtml = data.html; } - case 'renderedMarkup': - { - const cell = this.notebookEditor.getCellById(data.cellId); - if (cell instanceof MarkupCellViewModel) { - cell.renderedHtml = data.html; - } - this._handleHighlightCodeBlock(data.codeBlocks); - break; - } - case 'renderedCellOutput': - { - this._handleHighlightCodeBlock(data.codeBlocks); - break; - } + this._handleHighlightCodeBlock(data.codeBlocks); + break; + } + case 'renderedCellOutput': { + this._handleHighlightCodeBlock(data.codeBlocks); + break; + } } })); } @@ -1224,6 +1204,42 @@ var requirejs = (function() { this.reversedInsetMapping.set(message.outputId, content.source); } + async updateOutput(cellInfo: T, content: IInsetRenderOutput, cellTop: number, offset: number) { + if (this._disposed) { + return; + } + + if (!this.insetMapping.has(content.source)) { + this.createOutput(cellInfo, content, cellTop, offset); + return; + } + + const outputCache = this.insetMapping.get(content.source)!; + this.hiddenInsetMapping.delete(content.source); + let updatedContent: ICreationContent | undefined = undefined; + if (content.type === RenderOutputType.Extension) { + const output = content.source.model; + const first = output.outputs.find(op => op.mime === content.mimeType)!; + updatedContent = { + type: RenderOutputType.Extension, + outputId: outputCache.outputId, + mimeType: first.mime, + valueBytes: first.data.buffer, + metadata: output.metadata, + }; + } + + this._sendMessageToWebview({ + type: 'showOutput', + cellId: outputCache.cellInfo.cellId, + outputId: outputCache.outputId, + cellTop: cellTop, + outputOffset: offset, + content: updatedContent + }); + return; + } + removeInsets(outputs: readonly ICellOutputViewModel[]) { if (this._disposed) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 94b34a63d5f..743facf0e40 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -7,20 +7,13 @@ import { PixelRatio } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { Codicon, CSSIcon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; -import { combinedDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import * as platform from 'vs/base/common/platform'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import * as languages from 'vs/editor/common/languages'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; -import { tokenizeLineToHTML } from 'vs/editor/common/languages/textToHtmlTokenizer'; -import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -30,21 +23,26 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { CodeCellLayoutInfo, EXPAND_CELL_OUTPUT_COMMAND_ID, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys'; +import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellComments } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellComments'; +import { CellContextKeyPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys'; import { CellDecorations } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDecorations'; -import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; +import { CellDragAndDropController, CellDragAndDropPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; +import { CodeCellDragImageRenderer } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDragRenderer'; import { CellEditorOptions } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions'; import { CellExecutionPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution'; +import { CellFocusPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellFocus'; import { CellFocusIndicator } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator'; import { CellProgressBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar'; import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart'; import { BetweenCellToolbar, CellTitleToolbarPart } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellToolbars'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/cellParts/codeCell'; import { RunToolbar } from 'vs/workbench/contrib/notebook/browser/view/cellParts/codeCellRunToolbar'; +import { CollapsedCellInput } from 'vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellInput'; +import { CollapsedCellOutput } from 'vs/workbench/contrib/notebook/browser/view/cellParts/collapsedCellOutput'; import { FoldedCellHint } from 'vs/workbench/contrib/notebook/browser/view/cellParts/foldedCellHint'; import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/cellParts/markdownCell'; -import { BaseCellRenderTemplate, CodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; +import { CodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; @@ -107,51 +105,6 @@ abstract class AbstractCellRenderer { this.editorOptions.dispose(); this.dndController = undefined; } - - protected commonRenderTemplate(templateData: BaseCellRenderTemplate): void { - templateData.templateDisposables.add(DOM.addDisposableListener(templateData.container, DOM.EventType.FOCUS, () => { - if (templateData.currentRenderedCell) { - this.notebookEditor.focusElement(templateData.currentRenderedCell); - } - }, true)); - } - - protected commonRenderElement(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { - this.dndController?.renderElement(element, templateData); - templateData.elementDisposables.add(new CellDecorations(templateData.rootContainer, templateData.decorationContainer, element)); - templateData.elementDisposables.add(templateData.instantiationService.createInstance(CellContextKeyManager, this.notebookEditor, element)); - } - - protected addCommonCollapseListeners(templateData: BaseCellRenderTemplate): IDisposable { - const collapsedPartListener = DOM.addDisposableListener(templateData.cellInputCollapsedContainer, DOM.EventType.DBLCLICK, e => { - const cell = templateData.currentRenderedCell; - if (!cell || !this.notebookEditor.hasModel()) { - return; - } - - if (cell.isInputCollapsed) { - cell.isInputCollapsed = false; - } else { - cell.isOutputCollapsed = false; - } - }); - - const clickHandler = DOM.addDisposableListener(templateData.cellInputCollapsedContainer, DOM.EventType.CLICK, e => { - const cell = templateData.currentRenderedCell; - if (!cell || !this.notebookEditor.hasModel()) { - return; - } - - const element = e.target as HTMLElement; - - if (element && element.classList && element.classList.contains('expandInputIcon')) { - // clicked on the expand icon - cell.isInputCollapsed = false; - } - }); - - return combinedDisposable(collapsedPartListener, clickHandler); - } } export class MarkupCellRenderer extends AbstractCellRenderer implements IListRenderer { @@ -194,7 +147,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const cellInputCollapsedContainer = DOM.append(codeInnerContent, $('.input-collapse-container')); const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); editorPart.style.display = 'none'; - + const cellCommentPartContainer = DOM.append(container, $('.cell-comment-container')); const innerContent = DOM.append(container, $('.cell.markdown')); const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); @@ -208,34 +161,37 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen rootClassDelegate, this.notebookEditor.creationOptions.menuIds.cellTitleToolbar, this.notebookEditor)); - const betweenCellToolbar = templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellContainer)); const focusIndicatorBottom = new FastDomNode(DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'))); - const statusBar = templateDisposables.add(this.instantiationService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart)); - const foldedCellHint = templateDisposables.add(scopedInstaService.createInstance(FoldedCellHint, this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))); + + const cellParts = [ + titleToolbar, + templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellContainer)), + templateDisposables.add(this.instantiationService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, undefined)), + templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)), + templateDisposables.add(scopedInstaService.createInstance(FoldedCellHint, this.notebookEditor, DOM.append(container, $('.notebook-folded-hint')))), + templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), + templateDisposables.add(this.instantiationService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), + templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), + templateDisposables.add(new CellFocusPart(container, undefined, this.notebookEditor)), + templateDisposables.add(new CellDragAndDropPart(container)), + templateDisposables.add(this.instantiationService.createInstance(CellContextKeyPart, this.notebookEditor)), + ]; const templateData: MarkdownCellRenderTemplate = { rootContainer, cellInputCollapsedContainer, instantiationService: scopedInstaService, container, - decorationContainer, cellContainer: innerContent, editorPart, editorContainer, - focusIndicator: new CellFocusIndicator(this.notebookEditor, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom), foldingIndicator, templateDisposables, elementDisposables: new DisposableStore(), - betweenCellToolbar, - titleToolbar, - statusBar, - foldedCellHint, + cellParts, toJSON: () => { return {}; } }; - this.commonRenderTemplate(templateData); - templateDisposables.add(this.addCommonCollapseListeners(templateData)); - return templateData; } @@ -244,8 +200,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen throw new Error('The notebook editor is not attached with view model yet.'); } - this.commonRenderElement(element, templateData); - templateData.currentRenderedCell = element; templateData.currentEditor = undefined; templateData.editorPart.style.display = 'none'; @@ -255,13 +209,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen return; } - templateData.elementDisposables.add(templateData.instantiationService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, [ - templateData.betweenCellToolbar, - templateData.titleToolbar, - templateData.statusBar, - templateData.focusIndicator, - templateData.foldedCellHint - ], this.renderedEditors)); + templateData.elementDisposables.add(templateData.instantiationService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, this.renderedEditors)); } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { @@ -273,116 +221,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen } } -class EditorTextRenderer { - - private static _ttPolicy = window.trustedTypes?.createPolicy('cellRendererEditorText', { - createHTML(input) { return input; } - }); - - getRichText(editor: ICodeEditor, modelRange: Range): HTMLElement | null { - const model = editor.getModel(); - if (!model) { - return null; - } - - const colorMap = this.getDefaultColorMap(); - const fontInfo = editor.getOptions().get(EditorOption.fontInfo); - const fontFamilyVar = '--notebook-editor-font-family'; - const fontSizeVar = '--notebook-editor-font-size'; - const fontWeightVar = '--notebook-editor-font-weight'; - - const style = `` - + `color: ${colorMap[languages.ColorId.DefaultForeground]};` - + `background-color: ${colorMap[languages.ColorId.DefaultBackground]};` - + `font-family: var(${fontFamilyVar});` - + `font-weight: var(${fontWeightVar});` - + `font-size: var(${fontSizeVar});` - + `line-height: ${fontInfo.lineHeight}px;` - + `white-space: pre;`; - - const element = DOM.$('div', { style }); - - const fontSize = fontInfo.fontSize; - const fontWeight = fontInfo.fontWeight; - element.style.setProperty(fontFamilyVar, fontInfo.fontFamily); - element.style.setProperty(fontSizeVar, `${fontSize}px`); - element.style.setProperty(fontWeightVar, fontWeight); - - const linesHtml = this.getRichTextLinesAsHtml(model, modelRange, colorMap); - element.innerHTML = linesHtml as string; - return element; - } - - private getRichTextLinesAsHtml(model: ITextModel, modelRange: Range, colorMap: string[]): string | TrustedHTML { - const startLineNumber = modelRange.startLineNumber; - const startColumn = modelRange.startColumn; - const endLineNumber = modelRange.endLineNumber; - const endColumn = modelRange.endColumn; - - const tabSize = model.getOptions().tabSize; - - let result = ''; - - for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); - const lineContent = lineTokens.getLineContent(); - const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0); - const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length); - - if (lineContent === '') { - result += '
'; - } else { - result += tokenizeLineToHTML(lineContent, lineTokens.inflate(), colorMap, startOffset, endOffset, tabSize, platform.isWindows); - } - } - - return EditorTextRenderer._ttPolicy?.createHTML(result) ?? result; - } - - private getDefaultColorMap(): string[] { - const colorMap = languages.TokenizationRegistry.getColorMap(); - const result: string[] = ['#000000']; - if (colorMap) { - for (let i = 1, len = colorMap.length; i < len; i++) { - result[i] = Color.Format.CSS.formatHex(colorMap[i]); - } - } - return result; - } -} - -class CodeCellDragImageRenderer { - getDragImage(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement { - let dragImage = this.getDragImageImpl(templateData, editor, type); - if (!dragImage) { - // TODO@roblourens I don't think this can happen - dragImage = document.createElement('div'); - dragImage.textContent = '1 cell'; - } - - return dragImage; - } - - private getDragImageImpl(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement | null { - const dragImageContainer = templateData.container.cloneNode(true) as HTMLElement; - dragImageContainer.classList.forEach(c => dragImageContainer.classList.remove(c)); - dragImageContainer.classList.add('cell-drag-image', 'monaco-list-row', 'focused', `${type}-cell-row`); - - const editorContainer: HTMLElement | null = dragImageContainer.querySelector('.cell-editor-container'); - if (!editorContainer) { - return null; - } - - const richEditorText = new EditorTextRenderer().getRichText(editor, new Range(1, 1, 1, 1000)); - if (!richEditorText) { - return null; - } - DOM.reset(editorContainer, richEditorText); - - return dragImageContainer; - } -} - export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; @@ -416,17 +254,14 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende // This is also the drag handle const focusIndicatorLeft = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'))); - const cellContainer = DOM.append(container, $('.cell.code')); const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); const cellInputCollapsedContainer = DOM.append(cellContainer, $('.input-collapse-container')); - - const runToolbar = templateDisposables.add(this.instantiationService.createInstance(RunToolbar, this.notebookEditor, contextKeyService, container, runButtonContainer)); const executionOrderLabel = DOM.append(focusIndicatorLeft.domNode, $('div.execution-count-label')); executionOrderLabel.title = localize('cellExecutionOrderCountLabel', 'Execution Order'); - const editorPart = DOM.append(cellContainer, $('.cell-editor-part')); const editorContainer = DOM.append(editorPart, $('.cell-editor-container')); + const cellCommentPartContainer = DOM.append(container, $('.cell-comment-container')); // create a special context key service that set the inCompositeEditor-contextkey const editorContextKeyService = templateDisposables.add(this.contextKeyServiceProvider(editorPart)); @@ -439,23 +274,16 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, - // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, { contributions: this.notebookEditor.creationOptions.cellEditorContributions }); templateDisposables.add(editor); - const progressBar = templateDisposables.add(this.instantiationService.createInstance(CellProgressBar, editorPart, cellInputCollapsedContainer)); - - const statusBar = templateDisposables.add(this.instantiationService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart)); - const outputContainer = new FastDomNode(DOM.append(container, $('.output'))); const cellOutputCollapsedContainer = DOM.append(outputContainer.domNode, $('.output-collapse-container')); const outputShowMoreContainer = new FastDomNode(DOM.append(container, $('.output-show-more-container'))); - const focusIndicatorRight = new FastDomNode(DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right'))); - const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink')); focusSinkElement.setAttribute('tabindex', '0'); const bottomCellToolbarContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); @@ -471,9 +299,25 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende rootClassDelegate, this.notebookEditor.creationOptions.menuIds.cellTitleToolbar, this.notebookEditor)); - const betweenCellToolbar = templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellToolbarContainer)); - const focusIndicatorPart = new CellFocusIndicator(this.notebookEditor, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom); + const focusIndicatorPart = templateDisposables.add(new CellFocusIndicator(this.notebookEditor, titleToolbar, focusIndicatorTop, focusIndicatorLeft, focusIndicatorRight, focusIndicatorBottom)); + const cellParts = [ + focusIndicatorPart, + titleToolbar, + templateDisposables.add(scopedInstaService.createInstance(BetweenCellToolbar, this.notebookEditor, titleToolbarContainer, bottomCellToolbarContainer)), + templateDisposables.add(this.instantiationService.createInstance(CellEditorStatusBar, this.notebookEditor, container, editorPart, editor)), + templateDisposables.add(this.instantiationService.createInstance(CellProgressBar, editorPart, cellInputCollapsedContainer)), + templateDisposables.add(this.instantiationService.createInstance(RunToolbar, this.notebookEditor, contextKeyService, container, runButtonContainer)), + templateDisposables.add(new CellDecorations(rootContainer, decorationContainer)), + templateDisposables.add(this.instantiationService.createInstance(CellComments, this.notebookEditor, cellCommentPartContainer)), + templateDisposables.add(new CellExecutionPart(this.notebookEditor, executionOrderLabel)), + templateDisposables.add(this.instantiationService.createInstance(CollapsedCellOutput, this.notebookEditor, cellOutputCollapsedContainer)), + templateDisposables.add(new CollapsedCellInput(this.notebookEditor, cellInputCollapsedContainer)), + templateDisposables.add(new CellFocusPart(container, focusSinkElement, this.notebookEditor)), + templateDisposables.add(new CellDragAndDropPart(container)), + templateDisposables.add(this.instantiationService.createInstance(CellContextKeyPart, this.notebookEditor)), + ]; + const templateData: CodeCellRenderTemplate = { rootContainer, editorPart, @@ -481,110 +325,29 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende cellOutputCollapsedContainer, instantiationService: scopedInstaService, container, - decorationContainer, cellContainer, - progressBar, - statusBar, - focusIndicator: focusIndicatorPart, - cellExecution: new CellExecutionPart(this.notebookEditor, executionOrderLabel), - titleToolbar, - betweenCellToolbar, focusSinkElement, - runToolbar, outputContainer, outputShowMoreContainer, editor, templateDisposables, elementDisposables: new DisposableStore(), + cellParts, toJSON: () => { return {}; } }; - this.setupOutputCollapsedPart(templateData); - // focusIndicatorLeft covers the left margin area // code/outputFocusIndicator need to be registered as drag handlers so their click handlers don't take over const dragHandles = [focusIndicatorLeft.domNode, focusIndicatorPart.codeFocusIndicator.domNode, focusIndicatorPart.outputFocusIndicator.domNode]; this.dndController?.registerDragHandle(templateData, rootContainer, dragHandles, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code')); - - templateDisposables.add(this.addCollapseClickCollapseHandler(templateData)); - templateDisposables.add(DOM.addDisposableListener(focusSinkElement, DOM.EventType.FOCUS, () => { - if (templateData.currentRenderedCell && (templateData.currentRenderedCell as CodeCellViewModel).outputsViewModels.length) { - this.notebookEditor.focusNotebookCell(templateData.currentRenderedCell, 'output'); - } - })); - - templateData.cellExecution.setup(templateData); - this.commonRenderTemplate(templateData); - return templateData; } - private setupOutputCollapsedPart(templateData: CodeCellRenderTemplate) { - const cellOutputCollapseContainer = templateData.cellOutputCollapsedContainer; - const placeholder = DOM.append(cellOutputCollapseContainer, $('span.expandOutputPlaceholder')) as HTMLElement; - placeholder.textContent = localize('cellOutputsCollapsedMsg', "Outputs are collapsed"); - const expandIcon = DOM.append(cellOutputCollapseContainer, $('span.expandOutputIcon')); - expandIcon.classList.add(...CSSIcon.asClassNameArray(Codicon.more)); - - const keybinding = this.keybindingService.lookupKeybinding(EXPAND_CELL_OUTPUT_COMMAND_ID); - if (keybinding) { - placeholder.title = localize('cellExpandOutputButtonLabelWithDoubleClick', "Double click to expand cell output ({0})", keybinding.getLabel()); - cellOutputCollapseContainer.title = localize('cellExpandOutputButtonLabel', "Expand Cell Output (${0})", keybinding.getLabel()); - } - - DOM.hide(cellOutputCollapseContainer); - - const expand = () => { - if (!templateData.currentRenderedCell) { - return; - } - - const textModel = this.notebookEditor.textModel!; - const index = textModel.cells.indexOf(templateData.currentRenderedCell.model); - - if (index < 0) { - return; - } - - templateData.currentRenderedCell.isOutputCollapsed = !templateData.currentRenderedCell.isOutputCollapsed; - }; - - templateData.templateDisposables.add(DOM.addDisposableListener(expandIcon, DOM.EventType.CLICK, () => { - expand(); - })); - - templateData.templateDisposables.add(DOM.addDisposableListener(cellOutputCollapseContainer, DOM.EventType.DBLCLICK, () => { - expand(); - })); - } - - private addCollapseClickCollapseHandler(templateData: CodeCellRenderTemplate): IDisposable { - const dragHandleListener = DOM.addDisposableListener(templateData.focusIndicator.left.domNode, DOM.EventType.DBLCLICK, e => { - const cell = templateData.currentRenderedCell; - if (!cell || !this.notebookEditor.hasModel()) { - return; - } - - const clickedOnInput = e.offsetY < (cell.layoutInfo as CodeCellLayoutInfo).outputContainerOffset; - if (clickedOnInput) { - cell.isInputCollapsed = !cell.isInputCollapsed; - } else { - cell.isOutputCollapsed = !cell.isOutputCollapsed; - } - }); - - const commonDisposables = this.addCommonCollapseListeners(templateData); - - return combinedDisposable(dragHandleListener, commonDisposables); - } - renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { if (!this.notebookEditor.hasModel()) { throw new Error('The notebook editor is not attached with view model yet.'); } - this.commonRenderElement(element, templateData); - templateData.currentRenderedCell = element; if (height === undefined) { @@ -594,49 +357,8 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.outputContainer.domNode.innerText = ''; templateData.outputContainer.domNode.appendChild(templateData.cellOutputCollapsedContainer); - const elementDisposables = templateData.elementDisposables; - - const codeCellView = elementDisposables.add(templateData.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData, [ - templateData.focusIndicator, - templateData.betweenCellToolbar, - templateData.statusBar, - templateData.progressBar, - templateData.titleToolbar, - templateData.runToolbar, - templateData.cellExecution - ])); - + templateData.elementDisposables.add(templateData.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); this.renderedEditors.set(element, templateData.editor); - - this.updateFocusIndicatorsForTitleMenuAndSubscribe(element, templateData, codeCellView); - } - - updateFocusIndicatorsForTitleMenuAndSubscribe(element: CodeCellViewModel, templateData: CodeCellRenderTemplate, codeCellView: CodeCell) { - // todo@rebornix, consolidate duplicated requests in next frame - templateData.elementDisposables.add(DOM.scheduleAtNextAnimationFrame(() => { - this.updateFocusIndicatorsForTitleMenu(templateData); - })); - - templateData.elementDisposables.add(element.onDidChangeLayout(() => { - templateData.elementDisposables.add(DOM.scheduleAtNextAnimationFrame(() => { - this.updateFocusIndicatorsForTitleMenu(templateData); - })); - })); - - templateData.elementDisposables.add(templateData.titleToolbar.onDidUpdateActions(() => { - this.updateFocusIndicatorsForTitleMenu(templateData); - })); - } - - private updateFocusIndicatorsForTitleMenu(templateData: CodeCellRenderTemplate): void { - const layoutInfo = this.notebookEditor.notebookOptions.getLayoutConfiguration(); - if (templateData.titleToolbar.hasActions) { - templateData.focusIndicator.left.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; - templateData.focusIndicator.right.domNode.style.transform = `translateY(${layoutInfo.editorToolbarHeight + layoutInfo.cellTopMargin}px)`; - } else { - templateData.focusIndicator.left.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; - templateData.focusIndicator.right.domNode.style.transform = `translateY(${layoutInfo.cellTopMargin}px)`; - } } disposeTemplate(templateData: CodeCellRenderTemplate): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index e81c877b0aa..8b98bba83f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -242,6 +242,7 @@ export interface IShowOutputMessage { readonly outputId: string; readonly cellTop: number; readonly outputOffset: number; + readonly content?: ICreationContent; } export interface IFocusOutputMessage { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 7303ddef0dd..43b31f662d1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -738,14 +738,12 @@ async function webviewPreloads(ctx: PreloadContext) { function createOutputItem( id: string, - element: HTMLElement, mime: string, metadata: unknown, valueBytes: Uint8Array ): rendererApi.OutputItem { - return Object.freeze({ + return Object.freeze({ id, - element, mime, metadata, @@ -1142,9 +1140,12 @@ async function webviewPreloads(ctx: PreloadContext) { break; } case 'showOutput': { - const { outputId, cellTop, cellId } = event.data; + const { outputId, cellTop, cellId, content } = event.data; outputRunner.enqueue(outputId, () => { viewModel.showOutput(cellId, outputId, cellTop); + if (content) { + viewModel.updateAndRerender(cellId, outputId, content); + } }); break; } @@ -1608,6 +1609,11 @@ async function webviewPreloads(ctx: PreloadContext) { cell?.show(outputId, top); } + public updateAndRerender(cellId: string, outputId: string, content: webviewMessages.ICreationContent) { + const cell = this._outputCells.get(cellId); + cell?.updateContentAndRerender(outputId, content); + } + public hideOutput(cellId: string) { const cell = this._outputCells.get(cellId); cell?.hide(); @@ -1913,6 +1919,10 @@ async function webviewPreloads(ctx: PreloadContext) { this.element.style.visibility = 'hidden'; } + public updateContentAndRerender(outputId: string, content: webviewMessages.ICreationContent) { + this.outputElements.get(outputId)?.updateContentAndRender(content); + } + public rerender() { for (const outputElement of this.outputElements.values()) { outputElement.rerender(); @@ -1978,6 +1988,10 @@ async function webviewPreloads(ctx: PreloadContext) { public rerender() { this._outputNode?.rerender(); } + + public updateContentAndRender(content: webviewMessages.ICreationContent) { + this._outputNode?.updateAndRerender(content); + } } vscode.postMessage({ @@ -2031,7 +2045,7 @@ async function webviewPreloads(ctx: PreloadContext) { } else { const rendererApi = preloadsAndErrors[0] as rendererApi.RendererApi; try { - rendererApi.renderOutputItem(createOutputItem(this.outputId, this.element, content.mimeType, content.metadata, content.valueBytes), this.element); + rendererApi.renderOutputItem(createOutputItem(this.outputId, content.mimeType, content.metadata, content.valueBytes), this.element); } catch (e) { showPreloadErrors(this.element, e); } @@ -2075,6 +2089,13 @@ async function webviewPreloads(ctx: PreloadContext) { this.render(this._content.content, this._content.preloadsAndErrors); } } + + public updateAndRerender(content: webviewMessages.ICreationContent) { + if (this._content) { + this._content.content = content; + this.render(this._content.content, this._content.preloadsAndErrors); + } + } } const markupCellDragManager = new class MarkupCellDragManager { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index a5f73d4c626..da35868b327 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -70,22 +70,6 @@ export abstract class BaseCellViewModel extends Disposable { private _editState: CellEditState = CellEditState.Preview; - // get editState(): CellEditState { - // return this._editState; - // } - - // set editState(newState: CellEditState) { - // if (newState === this._editState) { - // return; - // } - - // this._editState = newState; - // this._onDidChangeState.fire({ editStateChanged: true }); - // if (this._editState === CellEditState.Preview) { - // this.focusMode = CellFocusMode.Container; - // } - // } - private _lineNumbers: 'on' | 'off' | 'inherit' = 'inherit'; get lineNumbers(): 'on' | 'off' | 'inherit' { return this._lineNumbers; @@ -149,6 +133,7 @@ export abstract class BaseCellViewModel extends Disposable { set dragging(v: boolean) { this._dragging = v; + this._onDidChangeState.fire({ dragStateChanged: true }); } protected _textModelRef: IReference | undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts index 8a4f5280eb1..cc3aa9cbca9 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -22,6 +22,7 @@ export interface IViewCellEditingDelegate extends ITextCellEditingDelegate { export class JoinCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Join Cell'; + code: string = 'undoredo.notebooks.joinCell'; private _deletedRawCell: NotebookCellTextModel; constructor( public resource: URI, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index c5245e9ef02..5c9f559110d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -52,6 +52,16 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod throw new Error('editorHeight is write-only'); } + private _commentHeight = 0; + + set commentHeight(height: number) { + if (this._commentHeight === height) { + return; + } + this._commentHeight = height; + this.layoutChange({ commentHeight: true }, 'CodeCellViewModel#commentHeight'); + } + private _hoveringOutput: boolean = false; public get outputIsHovered(): boolean { return this._hoveringOutput; @@ -134,6 +144,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod ? this.viewContext.notebookOptions.computeCodeCellEditorWidth(initialNotebookLayoutInfo.width) : 0, statusBarHeight: 0, + commentHeight: 0, outputContainerOffset: 0, outputTotalHeight: 0, outputShowMoreContainerHeight: 0, @@ -167,6 +178,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod const bottomToolbarDimensions = this.viewContext.notebookOptions.computeBottomToolbarDimensions(); const outputShowMoreContainerHeight = state.outputShowMoreContainerHeight ? state.outputShowMoreContainerHeight : this._layoutInfo.outputShowMoreContainerHeight; const outputTotalHeight = Math.max(this._outputMinHeight, this.isOutputCollapsed ? notebookLayoutConfiguration.collapsedIndicatorHeight : this._outputsTop!.getTotalSum()); + const commentHeight = state.commentHeight ? this._commentHeight : this._layoutInfo.commentHeight; const originalLayout = this.layoutInfo; if (!this.isInputCollapsed) { @@ -210,6 +222,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorHeight, editorWidth, statusBarHeight, + commentHeight, outputContainerOffset, outputTotalHeight, outputShowMoreContainerHeight, @@ -230,6 +243,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + notebookLayoutConfiguration.collapsedIndicatorHeight + notebookLayoutConfiguration.cellBottomMargin //CELL_BOTTOM_MARGIN + bottomToolbarDimensions.bottomToolbarGap //BOTTOM_CELL_TOOLBAR_GAP + + commentHeight + outputTotalHeight + outputShowMoreContainerHeight; const outputShowMoreContainerOffset = totalHeight - bottomToolbarDimensions.bottomToolbarGap @@ -245,6 +259,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorHeight: this._layoutInfo.editorHeight, editorWidth, statusBarHeight: 0, + commentHeight, outputContainerOffset, outputTotalHeight, outputShowMoreContainerHeight, @@ -275,6 +290,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod editorHeight: this._layoutInfo.editorHeight, editorWidth: this._layoutInfo.editorWidth, statusBarHeight: this.layoutInfo.statusBarHeight, + commentHeight: this.layoutInfo.commentHeight, outputContainerOffset: this._layoutInfo.outputContainerOffset, outputTotalHeight: this._layoutInfo.outputTotalHeight, outputShowMoreContainerHeight: this._layoutInfo.outputShowMoreContainerHeight, @@ -339,6 +355,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod + layoutConfiguration.cellTopMargin + editorHeight + this.viewContext.notebookOptions.computeEditorStatusbarHeight(this.internalMetadata, this.uri) + + this._commentHeight + outputsTotalHeight + outputShowMoreContainerHeight + bottomToolbarGap diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 1e5302d296e..f897a762e74 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -317,7 +317,7 @@ export class NotebookEditorToolbar extends Disposable { } })); - this._reigsterNotebookActionsToolbar(); + this._registerNotebookActionsToolbar(); } private _buildBody() { @@ -338,7 +338,7 @@ export class NotebookEditorToolbar extends Disposable { DOM.append(this.domNode, this._notebookTopRightToolbarContainer); } - private _reigsterNotebookActionsToolbar() { + private _registerNotebookActionsToolbar() { this._notebookGlobalActionsMenu = this._register(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.notebookToolbar, this.contextKeyService)); this._register(this._notebookGlobalActionsMenu); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts new file mode 100644 index 00000000000..901106252c6 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookOverviewRuler.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as browser from 'vs/base/browser/browser'; +import { createFastDomNode, FastDomNode } from 'vs/base/browser/fastDomNode'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export class NotebookOverviewRuler extends Themable { + private readonly _domNode: FastDomNode; + private _lanes = 3; + + constructor(readonly notebookEditor: INotebookEditorDelegate, container: HTMLElement, @IThemeService themeService: IThemeService) { + super(themeService); + this._domNode = createFastDomNode(document.createElement('canvas')); + this._domNode.setPosition('relative'); + this._domNode.setLayerHinting(true); + this._domNode.setContain('strict'); + + container.appendChild(this._domNode.domNode); + + this._register(notebookEditor.onDidChangeDecorations(() => { + this.layout(); + })); + + this._register(browser.PixelRatio.onDidChange(() => { + this.layout(); + })); + } + + layout() { + const width = 10; + const layoutInfo = this.notebookEditor.getLayoutInfo(); + const scrollHeight = layoutInfo.scrollHeight; + const height = layoutInfo.height; + const ratio = browser.PixelRatio.value; + this._domNode.setWidth(width); + this._domNode.setHeight(height); + this._domNode.domNode.width = width * ratio; + this._domNode.domNode.height = height * ratio; + const ctx = this._domNode.domNode.getContext('2d')!; + ctx.clearRect(0, 0, width * ratio, height * ratio); + this._render(ctx, width * ratio, height * ratio, scrollHeight * ratio, ratio); + } + + private _render(ctx: CanvasRenderingContext2D, width: number, height: number, scrollHeight: number, ratio: number) { + const viewModel = this.notebookEditor._getViewModel(); + const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + const laneWidth = width / this._lanes; + + let currentFrom = 0; + + if (viewModel) { + for (let i = 0; i < viewModel.viewCells.length; i++) { + const viewCell = viewModel.viewCells[i]; + const textBuffer = viewCell.textBuffer; + const decorations = viewCell.getCellDecorations(); + const cellHeight = (viewCell.layoutInfo.totalHeight / scrollHeight) * ratio * height; + + decorations.filter(decoration => decoration.overviewRuler).forEach(decoration => { + const overviewRuler = decoration.overviewRuler!; + const fillStyle = this.getColor(overviewRuler.color)?.toString() || '#000000'; + const lineHeight = Math.min(fontInfo.lineHeight, (viewCell.layoutInfo.editorHeight / scrollHeight / textBuffer.getLineCount()) * ratio * height); + const lineNumbers = overviewRuler.modelRanges.map(range => range.startLineNumber).reduce((previous: number[], current: number) => { + if (previous.length === 0) { + previous.push(current); + } else { + const last = previous[previous.length - 1]; + if (last !== current) { + previous.push(current); + } + } + + return previous; + }, [] as number[]); + + for (let i = 0; i < lineNumbers.length; i++) { + ctx.fillStyle = fillStyle; + const lineNumber = lineNumbers[i]; + const offset = (lineNumber - 1) * lineHeight; + ctx.fillRect(laneWidth, currentFrom + offset, laneWidth, lineHeight); + } + + if (overviewRuler.includeOutput) { + ctx.fillStyle = fillStyle; + const outputOffset = (viewCell.layoutInfo.editorHeight / scrollHeight) * ratio * height; + const decorationHeight = (fontInfo.lineHeight / scrollHeight) * ratio * height; + ctx.fillRect(laneWidth, currentFrom + outputOffset, laneWidth, decorationHeight); + } + }); + + currentFrom += cellHeight; + } + } + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index 5bfc431dc34..3291110ac93 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -22,6 +22,7 @@ export interface ITextCellEditingDelegate { export class MoveCellEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Move Cell'; + code: string = 'undoredo.notebooks.moveCell'; constructor( public resource: URI, @@ -54,6 +55,7 @@ export class MoveCellEdit implements IResourceUndoRedoElement { export class SpliceCellsEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Insert Cell'; + code: string = 'undoredo.notebooks.insertCell'; constructor( public resource: URI, private diffs: [number, NotebookCellTextModel[], NotebookCellTextModel[]][], @@ -87,6 +89,7 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { export class CellMetadataEdit implements IResourceUndoRedoElement { type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; label: string = 'Update Cell Metadata'; + code: string = 'undoredo.notebooks.updateCellMetadata'; constructor( public resource: URI, readonly index: number, diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index ed57d528699..00b0622d2c7 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -26,6 +26,8 @@ import { isDefined } from 'vs/base/common/types'; class StackOperation implements IWorkspaceUndoRedoElement { type: UndoRedoElementType.Workspace; + readonly code = 'undoredo.notebooks.stackOperation'; + private _operations: IUndoRedoElement[] = []; private _beginSelectionState: ISelectionState | undefined = undefined; private _resultSelectionState: ISelectionState | undefined = undefined; @@ -699,6 +701,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return that.uri; } readonly label = 'Update Notebook Metadata'; + readonly code = 'undoredo.notebooks.updateCellMetadata'; undo() { that._updateNotebookMetadata(oldMetadata, false); } @@ -923,6 +926,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return that.uri; } readonly label = 'Update Cell Language'; + readonly code = 'undoredo.notebooks.updateCellLanguage'; undo() { that._changeCellLanguage(cell, oldLanguage, false); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index d9d6dca87e1..11a06a18b4e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -27,7 +27,7 @@ import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackupMeta, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; export const NOTEBOOK_EDITOR_ID = 'workbench.editor.notebook'; export const NOTEBOOK_DIFF_EDITOR_ID = 'workbench.editor.notebookTextDiffEditor'; @@ -757,7 +757,7 @@ export interface IResolvedNotebookEditorModel extends INotebookEditorModel { export interface INotebookEditorModel extends IEditorModel { readonly onDidChangeDirty: Event; - readonly onDidSave: Event; + readonly onDidSave: Event; readonly onDidChangeOrphaned: Event; readonly onDidChangeReadonly: Event; readonly resource: URI; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 2727986b54d..41ce758f5cf 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -13,7 +13,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID, IWorkingCopyIdentifier, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IResolvedWorkingCopyBackup, IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Schemas } from 'vs/base/common/network'; @@ -25,7 +25,7 @@ import { TaskSequentializer } from 'vs/base/common/async'; import { bufferToReadable, bufferToStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { assertType } from 'vs/base/common/types'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { canceled } from 'vs/base/common/errors'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; @@ -38,7 +38,7 @@ import { IUntitledFileWorkingCopy, IUntitledFileWorkingCopyModel, IUntitledFileW export class ComplexNotebookEditorModel extends EditorModel implements INotebookEditorModel { - private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); private readonly _onDidChangeDirty = this._register(new Emitter()); private readonly _onDidChangeContent = this._register(new Emitter()); @@ -95,6 +95,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook readonly capabilities = that._isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = that.onDidChangeDirty; readonly onDidChangeContent = that._onDidChangeContent.event; + readonly onDidSave = that.onDidSave; isDirty(): boolean { return that.isDirty(); } backup(token: CancellationToken): Promise { return that.backup(token); } save(): Promise { return that.save(); } @@ -385,7 +386,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook this._lastResolvedFileStat = await this._resolveStats(this.resource); if (success) { this.setDirty(false); - this._onDidSave.fire(); + this._onDidSave.fire({}); } })()).then(() => { return true; @@ -417,7 +418,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook return undefined; } this.setDirty(false); - this._onDidSave.fire(); + this._onDidSave.fire({}); return this._instantiationService.createInstance(NotebookEditorInput, targetResource, this.viewType, {}); } @@ -444,12 +445,12 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook export class SimpleNotebookEditorModel extends EditorModel implements INotebookEditorModel { private readonly _onDidChangeDirty = this._register(new Emitter()); - private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); private readonly _onDidChangeOrphaned = this._register(new Emitter()); private readonly _onDidChangeReadonly = this._register(new Emitter()); readonly onDidChangeDirty: Event = this._onDidChangeDirty.event; - readonly onDidSave: Event = this._onDidSave.event; + readonly onDidSave: Event = this._onDidSave.event; readonly onDidChangeOrphaned: Event = this._onDidChangeOrphaned.event; readonly onDidChangeReadonly: Event = this._onDidChangeReadonly.event; @@ -523,7 +524,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } } else { this._workingCopy = await this._workingCopyManager.resolve(this.resource, options?.forceReadFromFile ? { reload: { async: false, force: true } } : undefined); - this._workingCopyListeners.add(this._workingCopy.onDidSave(() => this._onDidSave.fire())); + this._workingCopyListeners.add(this._workingCopy.onDidSave(e => this._onDidSave.fire(e))); this._workingCopyListeners.add(this._workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); this._workingCopyListeners.add(this._workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire())); } diff --git a/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts new file mode 100644 index 00000000000..d936d148aa8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/cellDnd.test.ts @@ -0,0 +1,208 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { performCellDropEdits } from 'vs/workbench/contrib/notebook/browser/view/cellParts/cellDnd'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import * as assert from 'assert'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; + +interface IBeginningState { + startOrder: string[]; + selections: ICellRange[]; + focus: number; +} + +interface IDragAction { + dragIdx: number; + dragOverIdx: number; + direction: 'above' | 'below'; +} + +interface IEndState { + endOrder: string[]; + selection: ICellRange; + focus: number; +} + +async function testCellDnd(beginning: IBeginningState, dragAction: IDragAction, end: IEndState) { + await withTestNotebook( + beginning.startOrder.map(text => [text, 'plaintext', CellKind.Code, []]), + (editor, viewModel) => { + editor.setSelections(beginning.selections); + editor.setFocus({ start: beginning.focus, end: beginning.focus + 1 }); + performCellDropEdits(editor, viewModel.cellAt(dragAction.dragIdx)!, dragAction.direction, viewModel.cellAt(dragAction.dragOverIdx)!); + + for (let i in end.endOrder) { + assert.equal(viewModel.viewCells[i].getText(), end.endOrder[i]); + } + + assert.equal(editor.getSelections().length, 1); + assert.deepStrictEqual(editor.getSelections()[0], end.selection); + assert.deepStrictEqual(editor.getFocus(), { start: end.focus, end: end.focus + 1 }); + }); +} + +suite('cellDND', () => { + test('drag 1 cell', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 0, end: 1 }], + focus: 0 + }, + { + dragIdx: 0, + dragOverIdx: 1, + direction: 'below' + }, + { + endOrder: ['1', '0', '2', '3'], + selection: { start: 1, end: 2 }, + focus: 1 + } + ); + }); + + test('drag multiple contiguous cells down', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 1, end: 3 }], + focus: 1 + }, + { + dragIdx: 1, + dragOverIdx: 3, + direction: 'below' + }, + { + endOrder: ['0', '3', '1', '2'], + selection: { start: 2, end: 4 }, + focus: 2 + } + ); + }); + + test('drag multiple contiguous cells up', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 2, end: 4 }], + focus: 2 + }, + { + dragIdx: 3, + dragOverIdx: 0, + direction: 'above' + }, + { + endOrder: ['2', '3', '0', '1'], + selection: { start: 0, end: 2 }, + focus: 0 + } + ); + }); + + test('drag ranges down', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 0, end: 1 }, { start: 2, end: 3 }], + focus: 0 + }, + { + dragIdx: 0, + dragOverIdx: 3, + direction: 'below' + }, + { + endOrder: ['1', '3', '0', '2'], + selection: { start: 2, end: 4 }, + focus: 2 + } + ); + }); + + test('drag ranges up', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 1, end: 2 }, { start: 3, end: 4 }], + focus: 1 + }, + { + dragIdx: 1, + dragOverIdx: 0, + direction: 'above' + }, + { + endOrder: ['1', '3', '0', '2'], + selection: { start: 0, end: 2 }, + focus: 0 + } + ); + }); + + test('drag ranges between ranges', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 0, end: 1 }, { start: 3, end: 4 }], + focus: 0 + }, + { + dragIdx: 0, + dragOverIdx: 1, + direction: 'below' + }, + { + endOrder: ['1', '0', '3', '2'], + selection: { start: 1, end: 3 }, + focus: 1 + } + ); + }); + + test('drag ranges just above a range', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 1, end: 2 }, { start: 3, end: 4 }], + focus: 1 + }, + { + dragIdx: 1, + dragOverIdx: 1, + direction: 'above' + }, + { + endOrder: ['0', '1', '3', '2'], + selection: { start: 1, end: 3 }, + focus: 1 + } + ); + }); + + test('drag ranges inside a range', async () => { + await testCellDnd( + { + startOrder: ['0', '1', '2', '3'], + selections: [{ start: 0, end: 2 }, { start: 3, end: 4 }], + focus: 0 + }, + { + dragIdx: 0, + dragOverIdx: 0, + direction: 'below' + }, + { + endOrder: ['0', '1', '3', '2'], + selection: { start: 0, end: 3 }, + focus: 0 + } + ); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index 9103ade7a2b..2b59cccb488 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -374,7 +374,6 @@ suite('NotebookCommon', () => { assert.strictEqual(diffViewModels.viewModels[0].type, 'unchanged'); assert.strictEqual(diffViewModels.viewModels[0].checkIfOutputsModified(), false); assert.strictEqual(diffViewModels.viewModels[1].type, 'modified'); - assert.deepStrictEqual(diffViewModels.viewModels[1].checkIfOutputsModified(), { reason: undefined }); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 4fda839cb60..c1c39b0b693 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -9,9 +9,9 @@ import { mock } from 'vs/base/test/common/mock'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { FoldingModel, updateFoldingStateAtIndex } from 'vs/workbench/contrib/notebook/browser/viewModel/foldingModel'; import { expandCellRangesWithHiddenCells, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createNotebookCellList, setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; suite('ListViewInfoAccessor', () => { let disposables: DisposableStore; diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index 99807fee5a1..fd0d94ee157 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -37,8 +37,7 @@ import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/work import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { CellFindMatchWithIndex, IActiveNotebookEditorDelegate, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { ListViewInfoAccessor } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { ListViewInfoAccessor, NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; @@ -54,6 +53,7 @@ import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServic import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { ResourceMap } from 'vs/base/common/map'; import { TestClipboardService } from 'vs/platform/clipboard/test/common/testClipboardService'; +import { IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; export class TestCell extends NotebookCellTextModel { constructor( @@ -72,7 +72,7 @@ export class TestCell extends NotebookCellTextModel { export class NotebookEditorTestModel extends EditorModel implements INotebookEditorModel { private _dirty = false; - protected readonly _onDidSave = this._register(new Emitter()); + protected readonly _onDidSave = this._register(new Emitter()); readonly onDidSave = this._onDidSave.event; protected readonly _onDidChangeDirty = this._register(new Emitter()); @@ -139,7 +139,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi if (this._notebook) { this._dirty = false; this._onDidChangeDirty.fire(); - this._onDidSave.fire(); + this._onDidSave.fire({}); // todo, flush all states return true; } @@ -266,6 +266,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic const findMatches = viewModel.find(query, options).filter(match => match.matches.length > 0); return findMatches; } + override deltaCellDecorations() { return []; } }; return { editor: notebookEditor, viewModel }; diff --git a/src/vs/workbench/contrib/offline/browser/offline.contribution.ts b/src/vs/workbench/contrib/offline/browser/offline.contribution.ts index cf8a706ef33..075a47e02c8 100644 --- a/src/vs/workbench/contrib/offline/browser/offline.contribution.ts +++ b/src/vs/workbench/contrib/offline/browser/offline.contribution.ts @@ -19,19 +19,22 @@ import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/sta export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor('statusBar.offlineBackground', { dark: '#6c1717', light: '#6c1717', - hc: '#6c1717' + hcDark: '#6c1717', + hcLight: '#6c1717' }, localize('statusBarOfflineBackground', "Status bar background color when the workbench is offline. The status bar is shown in the bottom of the window")); export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineForeground', { dark: STATUS_BAR_FOREGROUND, light: STATUS_BAR_FOREGROUND, - hc: STATUS_BAR_FOREGROUND + hcDark: STATUS_BAR_FOREGROUND, + hcLight: STATUS_BAR_FOREGROUND }, localize('statusBarOfflineForeground', "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window")); export const STATUS_BAR_OFFLINE_BORDER = registerColor('statusBar.offlineBorder', { dark: STATUS_BAR_BORDER, light: STATUS_BAR_BORDER, - hc: STATUS_BAR_BORDER + hcDark: STATUS_BAR_BORDER, + hcLight: STATUS_BAR_BORDER }, localize('statusBarOfflineBorder', "Status bar border color separating to the sidebar and editor when the workbench is offline. The status bar is shown in the bottom of the window")); export class OfflineStatusBarController implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index e10ce89cdd8..ee9c7eadf5d 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -287,15 +287,15 @@ class SwitchOutputActionViewItem extends SelectActionViewItem { @IThemeService private readonly themeService: IThemeService, @IContextViewService contextViewService: IContextViewService ) { - super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', 'Output Channels.'), optionsAsChildren: true }); + super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', "Output Channels"), optionsAsChildren: true }); let outputChannelRegistry = Registry.as(Extensions.OutputChannels); - this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions())); - this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOtions())); - this._register(this.outputService.onActiveOutputChannel(() => this.updateOtions())); + this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOptions())); + this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOptions())); + this._register(this.outputService.onActiveOutputChannel(() => this.updateOptions())); this._register(attachSelectBoxStyler(this.selectBox, themeService)); - this.updateOtions(); + this.updateOptions(); } override render(container: HTMLElement): void { @@ -311,7 +311,7 @@ class SwitchOutputActionViewItem extends SelectActionViewItem { return channel ? channel.id : option; } - private updateOtions(): void { + private updateOptions(): void { const groups = groupBy(this.outputService.getChannelDescriptors(), (c1: IOutputChannelDescriptor, c2: IOutputChannelDescriptor) => { if (!c1.log && c2.log) { return -1; diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index e047966c95b..018b31c5768 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -9,7 +9,7 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; import { isIOS, OS } from 'vs/base/common/platform'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; -import { CheckboxActionViewItem } from 'vs/base/browser/ui/checkbox/checkbox'; +import { ToggleActionViewItem } from 'vs/base/browser/ui/toggle/toggle'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IAction, Action, Separator } from 'vs/base/common/actions'; @@ -35,7 +35,7 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { attachStylerCallback, attachInputBoxStyler, attachCheckboxStyler, attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; +import { attachStylerCallback, attachInputBoxStyler, attachToggleStyler, attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Emitter, Event } from 'vs/base/common/event'; @@ -57,7 +57,7 @@ type KeybindingEditorActionClassification = { const $ = DOM.$; -class ThemableCheckboxActionViewItem extends CheckboxActionViewItem { +class ThemableToggleActionViewItem extends ToggleActionViewItem { constructor(context: any, action: IAction, options: IActionViewItemOptions, private readonly themeService: IThemeService) { super(context, action, options); @@ -65,7 +65,7 @@ class ThemableCheckboxActionViewItem extends CheckboxActionViewItem { override render(container: HTMLElement): void { super.render(container); - this._register(attachCheckboxStyler(this.checkbox, this.themeService)); + this._register(attachToggleStyler(this.toggle, this.themeService)); } } @@ -375,7 +375,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const toolBar = this._register(new ToolBar(this.actionsContainer, this.contextMenuService, { actionViewItemProvider: (action: IAction) => { if (action.id === this.sortByPrecedenceAction.id || action.id === this.recordKeysAction.id) { - return new ThemableCheckboxActionViewItem(null, action, { keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel() }, this.themeService); + return new ThemableToggleActionViewItem(null, action, { keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel() }, this.themeService); } return undefined; }, @@ -1145,8 +1145,8 @@ class AccessibilityProvider implements IListAccessibilityProvider { diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css index f6d027c3373..6015cbd0607 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css @@ -43,7 +43,7 @@ .keybindings-editor > .keybindings-header > .search-container > .keybindings-search-actions-container .monaco-action-bar .action-item { margin-right: 4px; } -.keybindings-editor .monaco-action-bar .action-item .monaco-custom-checkbox { +.keybindings-editor .monaco-action-bar .action-item .monaco-custom-toggle { margin: 0; padding: 2px; } diff --git a/src/vs/workbench/contrib/preferences/browser/media/preferences.css b/src/vs/workbench/contrib/preferences/browser/media/preferences.css index 6a82d489745..364921e6fd3 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/preferences.css +++ b/src/vs/workbench/contrib/preferences/browser/media/preferences.css @@ -198,6 +198,10 @@ outline: 1px dotted #f38518; } +.monaco-editor.hc-light .settings-group-title-widget .title-container.focused { + outline: 1px dotted #0F4A85; +} + .monaco-editor .settings-group-title-widget .title-container .codicon { margin: 0 2px; width: 16px; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index f8a66626787..d6ea8e7732e 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -137,6 +137,10 @@ box-sizing: border-box; } +.settings-editor > .settings-body > .monaco-split-view2 { + margin-top: 14px; +} + .settings-editor.no-results > .settings-body .settings-toc-container, .settings-editor.no-results > .settings-body .settings-tree-container { display: none; @@ -281,7 +285,7 @@ } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .monaco-tl-contents { - max-width: 1200px; + max-width: min(100%, 1200px); /* We don't want the widgets to be too long */ margin: auto; box-sizing: border-box; padding-left: 24px; @@ -340,6 +344,10 @@ padding-left: 1px; } +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-label .codicon { + vertical-align: text-top; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .setting-item-overrides a.modified-scope { text-decoration: underline; cursor: pointer; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index 723b041b21f..f92d003cc0d 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -22,11 +22,6 @@ overflow: hidden; text-overflow: ellipsis; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key, -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-key { - margin-left: 4px; - min-width: 40%; -} .settings-editor > .settings-body .settings-tree-container .setting-item-bool .setting-list-object-input-key-checkbox { margin-left: 4px; @@ -39,17 +34,29 @@ cursor: pointer; } +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key { + margin-left: 4px; + width: 40%; +} .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-key { margin-left: 0; + min-width: 40%; } + .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-input-value, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-value { width: 100%; } -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-row:hover .setting-list-object-value, -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-row:focus .setting-list-object-value, -.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-row.selected .setting-list-object-value { - margin-right: 44px; + +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-row .setting-list-object-value, +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-value { + /* In case the text is too long, we don't want to block the pencil icon. */ + box-sizing: border-box; + padding-right: 40px; +} + +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-value { + width: 60%; } .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-value, @@ -153,6 +160,7 @@ margin-right: 4px; } +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-row, .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-edit-row { display: flex } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 7e598f83d5b..77276835939 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -25,7 +25,7 @@ import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/settingsEditor2'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationUpdateOverrides } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -43,7 +43,7 @@ import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/brow import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; @@ -99,7 +99,7 @@ export class SettingsEditor2 extends EditorPane { private static NARROW_TOTAL_WIDTH: number = SettingsEditor2.TOC_MIN_WIDTH + SettingsEditor2.EDITOR_MIN_WIDTH; private static MEDIUM_TOTAL_WIDTH: number = 1000; - private static readonly SUGGESTIONS: string[] = [ + private static SUGGESTIONS: string[] = [ `@${MODIFIED_SETTING_TAG}`, '@tag:notebookLayout', `@tag:${REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG}`, @@ -173,7 +173,7 @@ export class SettingsEditor2 extends EditorPane { private settingFastUpdateDelayer: Delayer; private settingSlowUpdateDelayer: Delayer; - private pendingSettingUpdate: { key: string; value: any } | null = null; + private pendingSettingUpdate: { key: string; value: any; languageFilter: string | undefined } | null = null; private readonly viewState: ISettingsEditorViewState; private _searchResultModel: SearchResultModel | null = null; @@ -199,6 +199,8 @@ export class SettingsEditor2 extends EditorPane { private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; + private searchWidgetWillBeDisposed = false; + constructor( @ITelemetryService telemetryService: ITelemetryService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, @@ -261,6 +263,10 @@ export class SettingsEditor2 extends EditorPane { })); this.modelDisposables = this._register(new DisposableStore()); + + if (ENABLE_LANGUAGE_FILTER && !SettingsEditor2.SUGGESTIONS.includes(`@${LANGUAGE_SETTING_TAG}`)) { + SettingsEditor2.SUGGESTIONS.push(`@${LANGUAGE_SETTING_TAG}`); + } } override get minimumWidth(): number { return SettingsEditor2.EDITOR_MIN_WIDTH; } @@ -339,6 +345,7 @@ export class SettingsEditor2 extends EditorPane { // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { this._register(input.onWillDispose(() => { + this.searchWidgetWillBeDisposed = true; this.searchWidget.setValue(''); })); @@ -788,7 +795,7 @@ export class SettingsEditor2 extends EditorPane { private createSettingsTree(container: HTMLElement): void { this.settingRenderers = this.instantiationService.createInstance(SettingTreeRenderers); - this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type))); + this._register(this.settingRenderers.onDidChangeSetting(e => this.onDidChangeSetting(e.key, e.value, e.type, e.manualReset))); this._register(this.settingRenderers.onDidOpenSettings(settingKey => { this.openSettingsFile({ revealSetting: { key: settingKey, edit: true } }); })); @@ -817,6 +824,9 @@ export class SettingsEditor2 extends EditorPane { // the element was not found } })); + this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { + this.focusSearch(`@${LANGUAGE_SETTING_TAG}${lang}`); + })); this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, container, @@ -869,16 +879,18 @@ export class SettingsEditor2 extends EditorPane { })); } - private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[]): void { + private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[], manualReset: boolean): void { + const parsedQuery = parseQuery(this.searchWidget.getValue()); + const languageFilter = parsedQuery.languageFilter; if (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key) { - this.updateChangedSetting(key, value); + this.updateChangedSetting(key, value, manualReset, languageFilter); } - this.pendingSettingUpdate = { key, value }; + this.pendingSettingUpdate = { key, value, languageFilter }; if (SettingsEditor2.shouldSettingUpdateFast(type)) { - this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value)); + this.settingFastUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter)); } else { - this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value)); + this.settingSlowUpdateDelayer.trigger(() => this.updateChangedSetting(key, value, manualReset, languageFilter)); } } @@ -948,19 +960,22 @@ export class SettingsEditor2 extends EditorPane { return ancestors.reverse(); } - private updateChangedSetting(key: string, value: any): Promise { + private updateChangedSetting(key: string, value: any, manualReset: boolean, languageFilter: string | undefined): Promise { // ConfigurationService displays the error if this fails. // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change const settingsTarget = this.settingsTargetsWidget.settingsTarget; const resource = URI.isUri(settingsTarget) ? settingsTarget : undefined; const configurationTarget = (resource ? ConfigurationTarget.WORKSPACE_FOLDER : settingsTarget); - const overrides: IConfigurationOverrides = { resource }; + const overrides: IConfigurationUpdateOverrides = { resource, overrideIdentifiers: languageFilter ? [languageFilter] : undefined }; - const isManualReset = value === undefined; + const configurationTargetIsWorkspace = configurationTarget === ConfigurationTarget.WORKSPACE || configurationTarget === ConfigurationTarget.WORKSPACE_FOLDER; - // If the user is changing the value back to the default, do a 'reset' instead + const userPassedInManualReset = configurationTargetIsWorkspace || !!languageFilter; + const isManualReset = userPassedInManualReset ? manualReset : value === undefined; + + // If the user is changing the value back to the default, and we're not targeting a workspace scope, do a 'reset' instead const inspected = this.configurationService.inspect(key, overrides); - if (inspected.defaultValue === value) { + if (!userPassedInManualReset && inspected.defaultValue === value) { value = undefined; } @@ -1031,13 +1046,13 @@ export class SettingsEditor2 extends EditorPane { /* __GDPR__ "settingsEditor.settingModified" : { - "key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "key" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The setting that is being modified." }, + "groupId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the setting is from the local search or remote search provider, if applicable." }, + "nlpIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the remote search provider results, if applicable." }, + "displayIndex" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The index of the setting in the combined search results, if applicable." }, + "showConfiguredOnly" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Whether the user is in the modified view, which shows configured settings only." }, + "isReset" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "Identifies whether a setting was reset to its default value." }, + "target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "rzhao271", "comment": "The scope of the setting, such as user or workspace." } } */ this.telemetryService.publicLog('settingsEditor.settingModified', data); @@ -1096,7 +1111,7 @@ export class SettingsEditor2 extends EditorPane { resolvedSettingsRoot.children!.push(await createTocTreeForExtensionSettings(this.extensionService, dividedGroups.extension || [])); if (!this.workspaceTrustManagementService.isWorkspaceTrusted() && (this.viewState.settingsTarget instanceof URI || this.viewState.settingsTarget === ConfigurationTarget.WORKSPACE)) { - const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.configurationService); + const configuredUntrustedWorkspaceSettings = resolveConfiguredUntrustedSettings(groups, this.viewState.settingsTarget, this.viewState.languageFilter, this.configurationService); if (configuredUntrustedWorkspaceSettings.length) { resolvedSettingsRoot.children!.unshift({ id: 'workspaceTrust', @@ -1240,8 +1255,8 @@ export class SettingsEditor2 extends EditorPane { } private async onSearchInputChanged(): Promise { - if (!this.currentSettingsModel) { - // Initializing search widget value + if (!this.currentSettingsModel || this.searchWidgetWillBeDisposed) { + // From initializing or disposing the search widget. return; } @@ -1264,6 +1279,7 @@ export class SettingsEditor2 extends EditorPane { this.viewState.extensionFilters = new Set(); this.viewState.featureFilters = new Set(); this.viewState.idFilters = new Set(); + this.viewState.languageFilter = undefined; if (query) { const parsedQuery = parseQuery(query); query = parsedQuery.query; @@ -1271,13 +1287,14 @@ export class SettingsEditor2 extends EditorPane { parsedQuery.extensionFilters.forEach(extensionId => this.viewState.extensionFilters!.add(extensionId)); parsedQuery.featureFilters!.forEach(feature => this.viewState.featureFilters!.add(feature)); parsedQuery.idFilters!.forEach(id => this.viewState.idFilters!.add(id)); + this.viewState.languageFilter = parsedQuery.languageFilter; } if (query && query !== '@') { query = this.parseSettingFromJSON(query) || query; return this.triggerFilterPreferences(query); } else { - if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size) { + if (this.viewState.tagFilters.size || this.viewState.extensionFilters.size || this.viewState.featureFilters.size || this.viewState.idFilters.size || this.viewState.languageFilter) { this.searchResultModel = this.createFilterModel(); } else { this.searchResultModel = null; @@ -1339,10 +1356,10 @@ export class SettingsEditor2 extends EditorPane { private reportFilteringUsed(query: string, results: ISearchResult[]): void { const nlpResult = results[SearchResultIdx.Remote]; - const nlpMetadata = nlpResult && nlpResult.metadata; + const nlpMetadata = nlpResult?.metadata; - const durations = { - nlpResult: nlpMetadata && nlpMetadata.duration + const duration = { + nlpResult: nlpMetadata?.duration }; // Count unique results @@ -1356,20 +1373,20 @@ export class SettingsEditor2 extends EditorPane { counts['nlpResult'] = nlpResult.filterMatches.length; } - const requestCount = nlpMetadata && nlpMetadata.requestCount; + const requestCount = nlpMetadata?.requestCount; const data = { - durations, + durations: duration, counts, requestCount }; /* __GDPR__ "settingsEditor.filter" : { - "durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "durations.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "How long the remote search provider took, if applicable." }, + "counts.nlpResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the remote search provider, if applicable." }, + "counts.filterResult" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of matches found by the local search provider, if applicable." }, + "requestCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "owner": "rzhao271", "comment": "The number of requests sent to Bing, if applicable." } } */ this.telemetryService.publicLog('settingsEditor.filter', data); @@ -1499,7 +1516,7 @@ export class SettingsEditor2 extends EditorPane { } else { /* __GDPR__ "settingsEditor.searchError" : { - "message": { "classification": "CallstackOrException", "purpose": "FeatureInsight" } + "message": { "classification": "CallstackOrException", "purpose": "FeatureInsight", "owner": "rzhao271", comment: "The error message of the search error." } } */ const message = getErrorMessage(err).trim(); @@ -1514,7 +1531,7 @@ export class SettingsEditor2 extends EditorPane { } private layoutTrees(dimension: DOM.Dimension): void { - const listHeight = dimension.height - (72 + 11 /* header height + editor padding */); + const listHeight = dimension.height - (72 + 11 + 14 /* header height + editor padding */); const settingsTreeHeight = listHeight; this.settingsTreeContainer.style.height = `${settingsTreeHeight}px`; this.settingsTree.layout(settingsTreeHeight, dimension.width); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index e5d204fe3e7..f4b4b41ad94 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -30,7 +30,7 @@ import { isArray, isDefined, isUndefinedOrNull } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, getLanguageTagSettingPlainKey, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -391,9 +391,9 @@ export function resolveSettingsTree(tocData: ITOCEntry, coreSettingsGrou }; } -export function resolveConfiguredUntrustedSettings(groups: ISettingsGroup[], target: SettingsTarget, configurationService: IWorkbenchConfigurationService): ISetting[] { +export function resolveConfiguredUntrustedSettings(groups: ISettingsGroup[], target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): ISetting[] { const allSettings = getFlatSettings(groups); - return [...allSettings].filter(setting => setting.restricted && inspectSetting(setting.key, target, configurationService).isConfigured); + return [...allSettings].filter(setting => setting.restricted && inspectSetting(setting.key, target, languageFilter, configurationService).isConfigured); } function compareNullableIntegers(a?: number, b?: number) { @@ -583,7 +583,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { context?: SettingsTreeSettingElement; containerElement: HTMLElement; categoryElement: HTMLElement; - labelElement: HTMLElement; + labelElement: SimpleIconLabel; descriptionElement: HTMLElement; controlElement: HTMLElement; deprecationWarningElement: HTMLElement; @@ -593,7 +593,7 @@ interface ISettingItemTemplate extends IDisposableTemplate { } interface ISettingBoolItemTemplate extends ISettingItemTemplate { - checkbox: Checkbox; + checkbox: Toggle; } interface ISettingTextItemTemplate extends ISettingItemTemplate { @@ -656,6 +656,7 @@ export interface ISettingChangeEvent { key: string; value: any; // undefined => reset/unconfigure type: SettingValueType | SettingValueType[]; + manualReset: boolean; } export interface ISettingLinkClickEvent { @@ -737,6 +738,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre protected readonly _onDidChangeSettingHeight = this._register(new Emitter()); readonly onDidChangeSettingHeight: Event = this._onDidChangeSettingHeight.event; + protected readonly _onApplyLanguageFilter = this._register(new Emitter()); + readonly onApplyLanguageFilter: Event = this._onApplyLanguageFilter.event; + private readonly markdownRenderer: MarkdownRenderer; constructor( @@ -775,13 +779,14 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const titleElement = DOM.append(container, $('.setting-item-title')); const labelCategoryContainer = DOM.append(titleElement, $('.setting-item-cat-label-container')); const categoryElement = DOM.append(labelCategoryContainer, $('span.setting-item-category')); - const labelElement = DOM.append(labelCategoryContainer, $('span.setting-item-label')); + const labelElementContainer = DOM.append(labelCategoryContainer, $('span.setting-item-label')); + const labelElement = new SimpleIconLabel(labelElementContainer); const miscLabel = new SettingsTreeMiscLabel(titleElement); const descriptionElement = DOM.append(container, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); + modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); const valueElement = DOM.append(container, $('.setting-item-value')); const controlElement = DOM.append(valueElement, $('div.setting-item-control')); @@ -844,7 +849,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre const toolbar = new ToolBar(container, this._contextMenuService, { toggleMenuTitle, renderDropdownAsChildElement: !isIOS, - moreIcon: settingsMoreActionIcon // change icon from ellipsis to gear + moreIcon: settingsMoreActionIcon }); return toolbar; } @@ -867,7 +872,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.categoryElement.textContent = element.displayCategory && (element.displayCategory + ': '); template.categoryElement.title = titleTooltip; - template.labelElement.textContent = element.displayLabel; + template.labelElement.text = element.displayLabel; template.labelElement.title = titleTooltip; template.descriptionElement.innerText = ''; @@ -882,7 +887,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.miscLabel.updateOtherOverrides(element, template.elementDisposables, this._onDidClickOverrideElement); - const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType }); + const onChange = (value: any) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType, manualReset: false }); const deprecationText = element.setting.deprecationMessage || ''; if (deprecationText && element.setting.deprecationMessageIsMarkdown) { const disposables = new DisposableStore(); @@ -1044,8 +1049,7 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I const openSettingsButton = new Button(common.controlElement, { title: true, buttonBackground: undefined, buttonHoverBackground: undefined }); common.toDispose.add(openSettingsButton); - common.toDispose.add(openSettingsButton.onDidClick(() => template.onChange!())); - openSettingsButton.label = SettingComplexRenderer.EDIT_IN_JSON_LABEL; + openSettingsButton.element.classList.add('edit-in-settings-button'); openSettingsButton.element.classList.add(AbstractSettingRenderer.CONTROL_CLASS); @@ -1074,10 +1078,28 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I } protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate, onChange: (value: string) => void): void { - template.onChange = () => this._onDidOpenSettings.fire(dataElement.setting.key); + const plainKey = getLanguageTagSettingPlainKey(dataElement.setting.key); + const editLanguageSettingLabel = localize('editLanguageSettingLabel', "Edit settings for {0}", plainKey); + const isLanguageTagSetting = dataElement.setting.isLanguageTagSetting; + template.button.label = isLanguageTagSetting + ? editLanguageSettingLabel + : SettingComplexRenderer.EDIT_IN_JSON_LABEL; + + template.elementDisposables.add(template.button.onDidClick(() => { + if (isLanguageTagSetting) { + this._onApplyLanguageFilter.fire(plainKey); + } else { + this._onDidOpenSettings.fire(dataElement.setting.key); + } + })); + this.renderValidations(dataElement, template); - template.button.element.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`); + if (isLanguageTagSetting) { + template.button.element.setAttribute('aria-label', editLanguageSettingLabel); + } else { + template.button.element.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`); + } } private renderValidations(dataElement: SettingsTreeSettingElement, template: ISettingComplexItemTemplate) { @@ -1448,7 +1470,8 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I this._onDidChangeSetting.fire({ key: template.context.setting.key, value: Object.keys(newValue).length === 0 ? undefined : sortKeys(newValue), - type: template.context.valueType + type: template.context.valueType, + manualReset: false }); } } @@ -1756,20 +1779,21 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const titleElement = DOM.append(container, $('.setting-item-title')); const categoryElement = DOM.append(titleElement, $('span.setting-item-category')); - const labelElement = DOM.append(titleElement, $('span.setting-item-label')); + const labelElementContainer = DOM.append(titleElement, $('span.setting-item-label')); + const labelElement = new SimpleIconLabel(labelElementContainer); const miscLabel = new SettingsTreeMiscLabel(titleElement); const descriptionAndValueElement = DOM.append(container, $('.setting-item-value-description')); const controlElement = DOM.append(descriptionAndValueElement, $('.setting-item-bool-control')); const descriptionElement = DOM.append(descriptionAndValueElement, $('.setting-item-description')); const modifiedIndicatorElement = DOM.append(container, $('.setting-item-modified-indicator')); - modifiedIndicatorElement.title = localize('modified', "Modified"); + modifiedIndicatorElement.title = localize('modified', "The setting has been configured in the current scope."); const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = new DisposableStore(); - const checkbox = new Checkbox({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: undefined }); + const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: undefined }); controlElement.appendChild(checkbox.domNode); toDispose.add(checkbox); toDispose.add(checkbox.onChange(() => { @@ -1888,6 +1912,8 @@ export class SettingTreeRenderers { readonly onDidChangeSettingHeight: Event; + readonly onApplyLanguageFilter: Event; + readonly allRenderers: ITreeRenderer[]; private readonly settingActions: IAction[]; @@ -1902,7 +1928,7 @@ export class SettingTreeRenderers { new Action('settings.resetSetting', localize('resetSettingLabel', "Reset Setting"), undefined, undefined, async context => { if (context instanceof SettingsTreeSettingElement) { if (!context.isUntrusted) { - this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined, type: context.setting.type as SettingValueType }); + this._onDidChangeSetting.fire({ key: context.setting.key, value: undefined, type: context.setting.type as SettingValueType, manualReset: true }); } } }), @@ -1935,6 +1961,7 @@ export class SettingTreeRenderers { this.onDidClickSettingLink = Event.any(...settingRenderers.map(r => r.onDidClickSettingLink)); this.onDidFocusSetting = Event.any(...settingRenderers.map(r => r.onDidFocusSetting)); this.onDidChangeSettingHeight = Event.any(...settingRenderers.map(r => r.onDidChangeSettingHeight)); + this.onApplyLanguageFilter = Event.any(...settingRenderers.map(r => r.onApplyLanguageFilter)); this.allRenderers = [ ...settingRenderers, @@ -2303,6 +2330,10 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate; featureFilters?: Set; idFilters?: Set; + languageFilter?: string; filterToCategory?: SettingsTreeGroupElement; } @@ -138,10 +139,16 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { tags?: Set; overriddenScopeList: string[] = []; + languageOverrideValues: Map> = new Map>(); description!: string; valueType!: SettingValueType; - constructor(setting: ISetting, parent: SettingsTreeGroupElement, inspectResult: IInspectResult, isWorkspaceTrusted: boolean) { + constructor( + setting: ISetting, + parent: SettingsTreeGroupElement, + inspectResult: IInspectResult, + isWorkspaceTrusted: boolean + ) { super(sanitizeId(parent.id + '_' + setting.key)); this.setting = setting; this.parent = parent; @@ -166,13 +173,13 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } private initLabels(): void { - const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id); + const displayKeyFormat = settingKeyToDisplayFormat(this.setting.key, this.parent!.id, this.setting.isLanguageTagSetting); this._displayLabel = displayKeyFormat.label; this._displayCategory = displayKeyFormat.category; } update(inspectResult: IInspectResult, isWorkspaceTrusted: boolean): void { - const { isConfigured, inspected, targetSelector } = inspectResult; + const { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector } = inspectResult; switch (targetSelector) { case 'workspaceFolderValue': @@ -181,7 +188,7 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { break; } - const displayValue = isConfigured ? inspected[targetSelector] : inspected.defaultValue; + let displayValue = isConfigured ? inspected[targetSelector] : inspected.defaultValue; const overriddenScopeList: string[] = []; if (targetSelector !== 'workspaceValue' && typeof inspected.workspaceValue !== 'undefined') { overriddenScopeList.push(localize('workspace', "Workspace")); @@ -195,9 +202,28 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { overriddenScopeList.push(localize('user', "User")); } - this.value = displayValue; - this.scopeValue = isConfigured && inspected[targetSelector]; - this.defaultValue = inspected.defaultValue; + if (inspected.overrideIdentifiers) { + for (const overrideIdentifier of inspected.overrideIdentifiers) { + const inspectedOverride = inspectedLanguageOverrides.get(overrideIdentifier); + if (inspectedOverride) { + this.languageOverrideValues.set(overrideIdentifier, inspectedOverride); + } + } + } + + if (languageSelector && this.languageOverrideValues.has(languageSelector)) { + const overrideValues = this.languageOverrideValues.get(languageSelector)!; + // In the worst case, go back to using the previous display value. + // Also, sometimes the override is in the form of a default value override, so consider that second. + displayValue = (isConfigured ? overrideValues[targetSelector] : overrideValues.defaultValue) ?? displayValue; + this.value = displayValue; + this.scopeValue = isConfigured && overrideValues[targetSelector]; + this.defaultValue = overrideValues.defaultValue ?? inspected.defaultValue; + } else { + this.value = displayValue; + this.scopeValue = isConfigured && inspected[targetSelector]; + this.defaultValue = inspected.defaultValue; + } this.isConfigured = isConfigured; if (isConfigured || this.setting.tags || this.tags || this.setting.restricted) { @@ -258,6 +284,8 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } else { this.valueType = SettingValueType.Object; } + } else if (this.setting.isLanguageTagSetting) { + this.valueType = SettingValueType.LanguageTag; } else { this.valueType = SettingValueType.Complex; } @@ -345,6 +373,23 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { } return idFilters.has(this.setting.key); } + + matchesAllLanguages(languageFilter?: string): boolean { + if (!languageFilter) { + // We're not filtering by language. + return true; + } + + // We have a language filter in the search widget at this point. + // We decide to show all language overridable settings to make the + // lang filter act more like a scope filter, + // rather than adding on an implicit @modified as well. + if (this.setting.scope === ConfigurationScope.LANGUAGE_OVERRIDABLE) { + return true; + } + + return false; + } } @@ -424,7 +469,7 @@ export class SettingsTreeModel { private updateSettings(settings: SettingsTreeSettingElement[]): void { settings.forEach(element => { - const inspectResult = inspectSetting(element.setting.key, this._viewState.settingsTarget, this._configurationService); + const inspectResult = inspectSetting(element.setting.key, this._viewState.settingsTarget, this._viewState.languageFilter, this._configurationService); element.update(inspectResult, this._isWorkspaceTrusted); }); } @@ -460,7 +505,7 @@ export class SettingsTreeModel { } private createSettingsTreeSettingElement(setting: ISetting, parent: SettingsTreeGroupElement): SettingsTreeSettingElement { - const inspectResult = inspectSetting(setting.key, this._viewState.settingsTarget, this._configurationService); + const inspectResult = inspectSetting(setting.key, this._viewState.settingsTarget, this._viewState.languageFilter, this._configurationService); const element = new SettingsTreeSettingElement(setting, parent, inspectResult, this._isWorkspaceTrusted); const nameElements = this._treeElementsBySettingName.get(setting.key) || []; @@ -474,15 +519,21 @@ interface IInspectResult { isConfigured: boolean; inspected: IConfigurationValue; targetSelector: 'userLocalValue' | 'userRemoteValue' | 'workspaceValue' | 'workspaceFolderValue'; + inspectedLanguageOverrides: Map>; + languageSelector: string | undefined; } -export function inspectSetting(key: string, target: SettingsTarget, configurationService: IWorkbenchConfigurationService): IInspectResult { +export function inspectSetting(key: string, target: SettingsTarget, languageFilter: string | undefined, configurationService: IWorkbenchConfigurationService): IInspectResult { const inspectOverrides = URI.isUri(target) ? { resource: target } : undefined; const inspected = configurationService.inspect(key, inspectOverrides); const targetSelector = target === ConfigurationTarget.USER_LOCAL ? 'userLocalValue' : target === ConfigurationTarget.USER_REMOTE ? 'userRemoteValue' : target === ConfigurationTarget.WORKSPACE ? 'workspaceValue' : 'workspaceFolderValue'; + const targetOverrideSelector = target === ConfigurationTarget.USER_LOCAL ? 'userLocal' : + target === ConfigurationTarget.USER_REMOTE ? 'userRemote' : + target === ConfigurationTarget.WORKSPACE ? 'workspace' : + 'workspaceFolder'; let isConfigured = typeof inspected[targetSelector] !== 'undefined'; if (!isConfigured) { if (target === ConfigurationTarget.USER_LOCAL) { @@ -496,14 +547,39 @@ export function inspectSetting(key: string, target: SettingsTarget, configuratio } } - return { isConfigured, inspected, targetSelector }; + const overrideIdentifiers = inspected.overrideIdentifiers; + const inspectedLanguageOverrides = new Map>(); + + // We must reset isConfigured to be false if languageFilter is set, and manually + // determine whether it can be set to true later. + if (languageFilter) { + isConfigured = false; + } + if (overrideIdentifiers) { + // The setting we're looking at has language overrides. + for (const overrideIdentifier of overrideIdentifiers) { + inspectedLanguageOverrides.set(overrideIdentifier, configurationService.inspect(key, { overrideIdentifier })); + } + + // For all language filters, see if there's an override for that filter. + if (languageFilter) { + if (inspectedLanguageOverrides.has(languageFilter)) { + const overrideValue = inspectedLanguageOverrides.get(languageFilter)![targetOverrideSelector]?.override; + if (typeof overrideValue !== 'undefined') { + isConfigured = true; + } + } + } + } + + return { isConfigured, inspected, targetSelector, inspectedLanguageOverrides, languageSelector: languageFilter }; } function sanitizeId(id: string): string { return id.replace(/[\.\/]/, '_'); } -export function settingKeyToDisplayFormat(key: string, groupId = ''): { category: string; label: string } { +export function settingKeyToDisplayFormat(key: string, groupId: string = '', isLanguageTagSetting: boolean = false): { category: string; label: string } { const lastDotIdx = key.lastIndexOf('.'); let category = ''; if (lastDotIdx >= 0) { @@ -515,6 +591,11 @@ export function settingKeyToDisplayFormat(key: string, groupId = ''): { category category = trimCategoryForGroup(category, groupId); category = wordifyKey(category); + if (isLanguageTagSetting) { + key = key.replace(/[\[\]]/g, ''); + key = '$(bracket) ' + key; + } + const label = wordifyKey(key); return { category, label }; } @@ -591,6 +672,7 @@ function trimCategoryForGroup(category: string, groupId: string): string { export function isExcludeSetting(setting: ISetting): boolean { return setting.key === 'files.exclude' || setting.key === 'search.exclude' || + setting.key === 'workbench.localHistory.exclude' || setting.key === 'files.watcherExclude'; } @@ -733,7 +815,7 @@ export class SearchResultModel extends SettingsTreeModel { const isRemote = !!this.environmentService.remoteAuthority; this.root.children = this.root.children - .filter(child => child instanceof SettingsTreeSettingElement && child.matchesAllTags(this._viewState.tagFilters) && child.matchesScope(this._viewState.settingsTarget, isRemote) && child.matchesAnyExtension(this._viewState.extensionFilters) && child.matchesAnyId(this._viewState.idFilters) && child.matchesAnyFeature(this._viewState.featureFilters)); + .filter(child => child instanceof SettingsTreeSettingElement && child.matchesAllTags(this._viewState.tagFilters) && child.matchesScope(this._viewState.settingsTarget, isRemote) && child.matchesAnyExtension(this._viewState.extensionFilters) && child.matchesAnyId(this._viewState.idFilters) && child.matchesAnyFeature(this._viewState.featureFilters) && child.matchesAllLanguages(this._viewState.languageFilter)); if (this.newExtensionSearchResults && this.newExtensionSearchResults.filterMatches.length) { const resultExtensionIds = this.newExtensionSearchResults.filterMatches @@ -765,17 +847,35 @@ export interface IParsedQuery { extensionFilters: string[]; idFilters: string[]; featureFilters: string[]; + languageFilter: string | undefined; } const tagRegex = /(^|\s)@tag:("([^"]*)"|[^"]\S*)/g; const extensionRegex = /(^|\s)@ext:("([^"]*)"|[^"]\S*)?/g; const featureRegex = /(^|\s)@feature:("([^"]*)"|[^"]\S*)?/g; const idRegex = /(^|\s)@id:("([^"]*)"|[^"]\S*)?/g; +const languageRegex = /(^|\s)@lang:("([^"]*)"|[^"]\S*)?/g; + export function parseQuery(query: string): IParsedQuery { + /** + * A helper function to parse the query on one type of regex. + * + * @param query The search query + * @param filterRegex The regex to use on the query + * @param parsedParts The parts that the regex parses out will be appended to the array passed in here. + * @returns The query with the parsed parts removed + */ + function getTagsForType(query: string, filterRegex: RegExp, parsedParts: string[]): string { + return query.replace(filterRegex, (_, __, quotedParsedElement, unquotedParsedElement) => { + const parsedElement: string = unquotedParsedElement || quotedParsedElement; + if (parsedElement) { + parsedParts.push(...parsedElement.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); + } + return ''; + }); + } + const tags: string[] = []; - const extensions: string[] = []; - const features: string[] = []; - const ids: string[] = []; query = query.replace(tagRegex, (_, __, quotedTag, tag) => { tags.push(tag || quotedTag); return ''; @@ -786,37 +886,27 @@ export function parseQuery(query: string): IParsedQuery { return ''; }); - query = query.replace(extensionRegex, (_, __, quotedExtensionId, extensionId) => { - const extensionIdQuery: string = extensionId || quotedExtensionId; - if (extensionIdQuery) { - extensions.push(...extensionIdQuery.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); - } - return ''; - }); + const extensions: string[] = []; + const features: string[] = []; + const ids: string[] = []; + const langs: string[] = []; + query = getTagsForType(query, extensionRegex, extensions); + query = getTagsForType(query, featureRegex, features); + query = getTagsForType(query, idRegex, ids); - query = query.replace(featureRegex, (_, __, quotedFeature, feature) => { - const featureQuery: string = feature || quotedFeature; - if (featureQuery) { - features.push(...featureQuery.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); - } - return ''; - }); - - query = query.replace(idRegex, (_, __, quotedId, id) => { - const idRegex: string = id || quotedId; - if (idRegex) { - ids.push(...idRegex.split(',').map(s => s.trim()).filter(s => !isFalsyOrWhitespace(s))); - } - return ''; - }); + if (ENABLE_LANGUAGE_FILTER) { + query = getTagsForType(query, languageRegex, langs); + } query = query.trim(); + // For now, only return the first found language filter return { tags, extensionFilters: extensions, featureFilters: features, idFilters: ids, - query + languageFilter: langs.length ? langs[0] : undefined, + query, }; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 4b31b923266..756f928dbc4 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { IAction } from 'vs/base/common/actions'; @@ -1353,7 +1353,7 @@ export class ObjectSettingCheckboxWidget extends AbstractListSettingWidget void ) { - const checkbox = new Checkbox({ + const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: value, diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index 6318c87aa7e..ec3a14f1bf4 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -75,6 +75,9 @@ export const MODIFIED_SETTING_TAG = 'modified'; export const EXTENSION_SETTING_TAG = 'ext:'; export const FEATURE_SETTING_TAG = 'feature:'; export const ID_SETTING_TAG = 'id:'; +export const LANGUAGE_SETTING_TAG = 'lang:'; export const WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; + +export const ENABLE_LANGUAGE_FILTER = true; diff --git a/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts b/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts index ea16173f169..1434def00dc 100644 --- a/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts +++ b/src/vs/workbench/contrib/preferences/common/settingsEditorColorRegistry.ts @@ -5,54 +5,58 @@ import { Color, RGBA } from 'vs/base/common/color'; import { localize } from 'vs/nls'; -import { editorWidgetBorder, focusBorder, inputBackground, inputBorder, inputForeground, listHoverBackground, registerColor, selectBackground, selectBorder, selectForeground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBorder, focusBorder, inputBackground, inputBorder, inputForeground, listHoverBackground, registerColor, selectBackground, selectBorder, selectForeground, checkboxBackground, checkboxBorder, checkboxForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; // General setting colors -export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); +export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hcDark: '#ffffff', hcLight: '#292929' }, localize('headerForeground', "The foreground color for a section header or active title.")); export const modifiedItemIndicator = registerColor('settings.modifiedItemIndicator', { light: new Color(new RGBA(102, 175, 224)), dark: new Color(new RGBA(12, 125, 157)), - hc: new Color(new RGBA(0, 73, 122)) + hcDark: new Color(new RGBA(0, 73, 122)), + hcLight: new Color(new RGBA(102, 175, 224)), }, localize('modifiedItemForeground', "The color of the modified setting indicator.")); -export const settingsHeaderBorder = registerColor('settings.headerBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hc: PANEL_BORDER }, localize('settingsHeaderBorder', "The color of the header container border.")); -export const settingsSashBorder = registerColor('settings.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hc: PANEL_BORDER }, localize('settingsSashBorder', "The color of the Settings editor splitview sash border.")); +export const settingsHeaderBorder = registerColor('settings.headerBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('settingsHeaderBorder', "The color of the header container border.")); +export const settingsSashBorder = registerColor('settings.sashBorder', { dark: PANEL_BORDER, light: PANEL_BORDER, hcDark: PANEL_BORDER, hcLight: PANEL_BORDER }, localize('settingsSashBorder', "The color of the Settings editor splitview sash border.")); // Enum control colors -export const settingsSelectBackground = registerColor(`settings.dropdownBackground`, { dark: selectBackground, light: selectBackground, hc: selectBackground }, localize('settingsDropdownBackground', "Settings editor dropdown background.")); -export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hc: selectForeground }, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); -export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hc: selectBorder }, localize('settingsDropdownBorder', "Settings editor dropdown border.")); -export const settingsSelectListBorder = registerColor('settings.dropdownListBorder', { dark: editorWidgetBorder, light: editorWidgetBorder, hc: editorWidgetBorder }, localize('settingsDropdownListBorder', "Settings editor dropdown list border. This surrounds the options and separates the options from the description.")); +export const settingsSelectBackground = registerColor(`settings.dropdownBackground`, { dark: selectBackground, light: selectBackground, hcDark: selectBackground, hcLight: selectBackground }, localize('settingsDropdownBackground', "Settings editor dropdown background.")); +export const settingsSelectForeground = registerColor('settings.dropdownForeground', { dark: selectForeground, light: selectForeground, hcDark: selectForeground, hcLight: selectForeground }, localize('settingsDropdownForeground', "Settings editor dropdown foreground.")); +export const settingsSelectBorder = registerColor('settings.dropdownBorder', { dark: selectBorder, light: selectBorder, hcDark: selectBorder, hcLight: selectBorder }, localize('settingsDropdownBorder', "Settings editor dropdown border.")); +export const settingsSelectListBorder = registerColor('settings.dropdownListBorder', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('settingsDropdownListBorder', "Settings editor dropdown list border. This surrounds the options and separates the options from the description.")); // Bool control colors -export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: simpleCheckboxBackground, light: simpleCheckboxBackground, hc: simpleCheckboxBackground }, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); -export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: simpleCheckboxForeground, light: simpleCheckboxForeground, hc: simpleCheckboxForeground }, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); -export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: simpleCheckboxBorder, light: simpleCheckboxBorder, hc: simpleCheckboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); +export const settingsCheckboxBackground = registerColor('settings.checkboxBackground', { dark: checkboxBackground, light: checkboxBackground, hcDark: checkboxBackground, hcLight: checkboxBackground }, localize('settingsCheckboxBackground', "Settings editor checkbox background.")); +export const settingsCheckboxForeground = registerColor('settings.checkboxForeground', { dark: checkboxForeground, light: checkboxForeground, hcDark: checkboxForeground, hcLight: checkboxForeground }, localize('settingsCheckboxForeground', "Settings editor checkbox foreground.")); +export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: checkboxBorder, light: checkboxBorder, hcDark: checkboxBorder, hcLight: checkboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); -export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); -export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); +export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hcDark: inputForeground, hcLight: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); +export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); -export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); -export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hcDark: inputForeground, hcLight: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); +export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); export const focusedRowBackground = registerColor('settings.focusedRowBackground', { dark: transparent(listHoverBackground, .6), light: transparent(listHoverBackground, .6), - hc: null + hcDark: null, + hcLight: null, }, localize('focusedRowBackground', "The background color of a settings row when focused.")); export const rowHoverBackground = registerColor('settings.rowHoverBackground', { dark: transparent(listHoverBackground, .3), light: transparent(listHoverBackground, .3), - hc: null + hcDark: null, + hcLight: null }, localize('settings.rowHoverBackground', "The background color of a settings row when hovered.")); export const focusedRowBorder = registerColor('settings.focusedRowBorder', { dark: Color.white.transparent(0.12), light: Color.black.transparent(0.12), - hc: focusBorder + hcDark: focusBorder, + hcLight: focusBorder }, localize('settings.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); diff --git a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts index 7d766f7acbd..8d8d19abc03 100644 --- a/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts +++ b/src/vs/workbench/contrib/preferences/test/browser/settingsTreeModels.test.ts @@ -149,7 +149,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -159,7 +160,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -169,7 +171,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -179,7 +182,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: 'foo', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -189,7 +193,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -199,7 +204,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: 'my query', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -209,7 +215,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: 'test query', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -219,7 +226,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: 'test', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -229,7 +237,8 @@ suite('SettingsTree', () => { extensionFilters: [], query: 'query has @ for some reason', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -239,7 +248,8 @@ suite('SettingsTree', () => { extensionFilters: ['github.vscode-pull-request-github'], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -249,7 +259,8 @@ suite('SettingsTree', () => { extensionFilters: ['github.vscode-pull-request-github', 'vscode.git'], query: '', featureFilters: [], - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( '@feature:scm', @@ -258,7 +269,8 @@ suite('SettingsTree', () => { extensionFilters: [], featureFilters: ['scm'], query: '', - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( @@ -268,7 +280,8 @@ suite('SettingsTree', () => { extensionFilters: [], featureFilters: ['scm', 'terminal'], query: '', - idFilters: [] + idFilters: [], + languageFilter: undefined }); testParseQuery( '@id:files.autoSave', @@ -277,7 +290,8 @@ suite('SettingsTree', () => { extensionFilters: [], featureFilters: [], query: '', - idFilters: ['files.autoSave'] + idFilters: ['files.autoSave'], + languageFilter: undefined }); testParseQuery( @@ -287,7 +301,30 @@ suite('SettingsTree', () => { extensionFilters: [], featureFilters: [], query: '', - idFilters: ['files.autoSave', 'terminal.integrated.commandsToSkipShell'] + idFilters: ['files.autoSave', 'terminal.integrated.commandsToSkipShell'], + languageFilter: undefined + }); + + testParseQuery( + '@lang:cpp', + { + tags: [], + extensionFilters: [], + featureFilters: [], + query: '', + idFilters: [], + languageFilter: 'cpp' + }); + + testParseQuery( + '@lang:cpp,python', + { + tags: [], + extensionFilters: [], + featureFilters: [], + query: '', + idFilters: [], + languageFilter: 'cpp' }); }); }); diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index 1cad625e6ae..d1a3d7680d5 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -149,7 +149,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider { const paneComposites = this.paneCompositeService.getPaneComposites(location); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 63136a9f049..e6b3c873d57 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -260,7 +260,7 @@ class LocalAddressColumn implements ITableColumn { const markdown = new MarkdownString('', true); const uri = localAddress.startsWith('http') ? localAddress : `http://${localAddress}`; - return markdown.appendMarkdown(`[Follow link](${uri}) (${clickLabel})`); + return markdown.appendLink(uri, 'Follow link').appendMarkdown(` (${clickLabel})`); }; } } @@ -1692,7 +1692,8 @@ MenuRegistry.appendMenuItem(MenuId.TunnelLocalAddressInline, ({ export const portWithRunningProcessForeground = registerColor('ports.iconRunningProcessForeground', { light: STATUS_BAR_HOST_NAME_BACKGROUND, dark: STATUS_BAR_HOST_NAME_BACKGROUND, - hc: STATUS_BAR_HOST_NAME_BACKGROUND + hcDark: STATUS_BAR_HOST_NAME_BACKGROUND, + hcLight: STATUS_BAR_HOST_NAME_BACKGROUND }, nls.localize('portWithRunningProcess.foreground', "The color of the icon for a port that has an associated running process.")); registerThemingParticipant((theme, collector) => { diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 7ba5f021196..894ccb8be73 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -54,7 +54,7 @@ export class LabelContribution implements IWorkbenchContribution { if (remoteEnvironment) { this.labelService.registerFormatter({ - scheme: Schemas.userData, + scheme: Schemas.vscodeUserData, formatting }); } diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index b37bc3a192d..055172e9e9b 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -20,8 +20,7 @@ import { URI } from 'vs/base/common/uri'; import { ISCMService, ISCMRepository, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, themeColorFromId, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; -import { Color, RGBA } from 'vs/base/common/color'; +import { editorBackground, editorErrorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions'; import { PeekViewWidget, getOuterEditor, peekViewBorder, peekViewTitleBackground, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/browser/peekView'; @@ -52,6 +51,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { TextCompareEditorActiveContext } from 'vs/workbench/common/contextkeys'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { IChange } from 'vs/editor/common/diff/diffComputer'; +import { Color } from 'vs/base/common/color'; class DiffActionRunner extends ActionRunner { @@ -849,44 +849,50 @@ export class DirtyDiffController extends Disposable implements IEditorContributi } export const editorGutterModifiedBackground = registerColor('editorGutter.modifiedBackground', { - dark: new Color(new RGBA(12, 125, 157)), - light: new Color(new RGBA(102, 175, 224)), - hc: new Color(new RGBA(0, 155, 249)) + dark: '#1B81A8', + light: '#2090D3', + hcDark: '#1B81A8', + hcLight: '#2090D3' }, nls.localize('editorGutterModifiedBackground', "Editor gutter background color for lines that are modified.")); export const editorGutterAddedBackground = registerColor('editorGutter.addedBackground', { - dark: new Color(new RGBA(88, 124, 12)), - light: new Color(new RGBA(129, 184, 139)), - hc: new Color(new RGBA(51, 171, 78)) + dark: '#487E02', + light: '#48985D', + hcDark: '#487E02', + hcLight: '#48985D' }, nls.localize('editorGutterAddedBackground', "Editor gutter background color for lines that are added.")); export const editorGutterDeletedBackground = registerColor('editorGutter.deletedBackground', { - dark: new Color(new RGBA(148, 21, 27)), - light: new Color(new RGBA(202, 75, 81)), - hc: new Color(new RGBA(252, 93, 109)) + dark: editorErrorForeground, + light: editorErrorForeground, + hcDark: editorErrorForeground, + hcLight: editorErrorForeground }, nls.localize('editorGutterDeletedBackground', "Editor gutter background color for lines that are deleted.")); export const minimapGutterModifiedBackground = registerColor('minimapGutter.modifiedBackground', { - dark: new Color(new RGBA(12, 125, 157)), - light: new Color(new RGBA(102, 175, 224)), - hc: new Color(new RGBA(0, 155, 249)) + dark: editorGutterModifiedBackground, + light: editorGutterModifiedBackground, + hcDark: editorGutterModifiedBackground, + hcLight: editorGutterModifiedBackground }, nls.localize('minimapGutterModifiedBackground', "Minimap gutter background color for lines that are modified.")); export const minimapGutterAddedBackground = registerColor('minimapGutter.addedBackground', { - dark: new Color(new RGBA(88, 124, 12)), - light: new Color(new RGBA(129, 184, 139)), - hc: new Color(new RGBA(51, 171, 78)) + dark: editorGutterAddedBackground, + light: editorGutterAddedBackground, + hcDark: editorGutterAddedBackground, + hcLight: editorGutterAddedBackground }, nls.localize('minimapGutterAddedBackground', "Minimap gutter background color for lines that are added.")); export const minimapGutterDeletedBackground = registerColor('minimapGutter.deletedBackground', { - dark: new Color(new RGBA(148, 21, 27)), - light: new Color(new RGBA(202, 75, 81)), - hc: new Color(new RGBA(252, 93, 109)) + dark: editorGutterDeletedBackground, + light: editorGutterDeletedBackground, + hcDark: editorGutterDeletedBackground, + hcLight: editorGutterDeletedBackground }, nls.localize('minimapGutterDeletedBackground', "Minimap gutter background color for lines that are deleted.")); -export const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', { dark: transparent(editorGutterModifiedBackground, 0.6), light: transparent(editorGutterModifiedBackground, 0.6), hc: transparent(editorGutterModifiedBackground, 0.6) }, nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); -export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', { dark: transparent(editorGutterAddedBackground, 0.6), light: transparent(editorGutterAddedBackground, 0.6), hc: transparent(editorGutterAddedBackground, 0.6) }, nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); -export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', { dark: transparent(editorGutterDeletedBackground, 0.6), light: transparent(editorGutterDeletedBackground, 0.6), hc: transparent(editorGutterDeletedBackground, 0.6) }, nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); +export const overviewRulerModifiedForeground = registerColor('editorOverviewRuler.modifiedForeground', { dark: transparent(editorGutterModifiedBackground, 0.6), light: transparent(editorGutterModifiedBackground, 0.6), hcDark: transparent(editorGutterModifiedBackground, 0.6), hcLight: transparent(editorGutterModifiedBackground, 0.6) }, nls.localize('overviewRulerModifiedForeground', 'Overview ruler marker color for modified content.')); +export const overviewRulerAddedForeground = registerColor('editorOverviewRuler.addedForeground', { dark: transparent(editorGutterAddedBackground, 0.6), light: transparent(editorGutterAddedBackground, 0.6), hcDark: transparent(editorGutterAddedBackground, 0.6), hcLight: transparent(editorGutterAddedBackground, 0.6) }, nls.localize('overviewRulerAddedForeground', 'Overview ruler marker color for added content.')); +export const overviewRulerDeletedForeground = registerColor('editorOverviewRuler.deletedForeground', { dark: transparent(editorGutterDeletedBackground, 0.6), light: transparent(editorGutterDeletedBackground, 0.6), hcDark: transparent(editorGutterDeletedBackground, 0.6), hcLight: transparent(editorGutterDeletedBackground, 0.6) }, nls.localize('overviewRulerDeletedForeground', 'Overview ruler marker color for deleted content.')); class DirtyDiffDecorator extends Disposable { @@ -1461,15 +1467,22 @@ export class DirtyDiffWorkbenchController extends Disposable implements ext.IWor registerEditorContribution(DirtyDiffController.ID, DirtyDiffController); registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const editorBackgroundColor = theme.getColor(editorBackground); const editorGutterModifiedBackgroundColor = theme.getColor(editorGutterModifiedBackground); + const linearGradient = `-45deg, ${editorGutterModifiedBackgroundColor} 25%, ${editorBackgroundColor} 25%, ${editorBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 50%, ${editorGutterModifiedBackgroundColor} 75%, ${editorBackgroundColor} 75%, ${editorBackgroundColor}`; + if (editorGutterModifiedBackgroundColor) { collector.addRule(` .monaco-editor .dirty-diff-modified { - border-left: 3px solid ${editorGutterModifiedBackgroundColor}; + background-size: 3px 4.5px; + background-repeat-x: no-repeat; + background-image: linear-gradient(${linearGradient}); transition: opacity 0.5s; } .monaco-editor .dirty-diff-modified:before { - background: ${editorGutterModifiedBackgroundColor}; + transform: translateX(3px); + background-size: 3px 3px; + background-image: linear-gradient(${linearGradient}); } .monaco-editor .margin:hover .dirty-diff-modified { opacity: 1; diff --git a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css index dd0921f4d60..833aa22b57a 100644 --- a/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css +++ b/src/vs/workbench/contrib/scm/browser/media/dirtydiffDecorator.css @@ -28,7 +28,7 @@ height: 100%; width: 0; left: -2px; - transition: width 80ms linear, left 80ms linear; + transition: width 80ms linear, left 80ms linear, transform 80ms linear; } .monaco-editor .dirty-diff-deleted:before { diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index bfb89c415f4..e66a0f24fd5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $ } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list'; -import { ISCMRepository, ISCMService, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -41,7 +41,6 @@ export class SCMRepositoriesViewPane extends ViewPane { constructor( options: IViewPaneOptions, - @ISCMService protected scmService: ISCMService, @ISCMViewService protected scmViewService: ISCMViewService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -85,15 +84,9 @@ export class SCMRepositoriesViewPane extends ViewPane { this._register(this.list.onDidChangeSelection(this.onListSelectionChange, this)); this._register(this.list.onContextMenu(this.onListContextMenu, this)); + this._register(this.scmViewService.onDidChangeRepositories(this.onDidChangeRepositories, this)); this._register(this.scmViewService.onDidChangeVisibleRepositories(this.updateListSelection, this)); - this._register(this.scmService.onDidAddRepository(this.onDidAddRepository, this)); - this._register(this.scmService.onDidRemoveRepository(this.onDidRemoveRepository, this)); - - for (const repository of this.scmService.repositories) { - this.onDidAddRepository(repository); - } - if (this.orientation === Orientation.VERTICAL) { this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.repositories.visible')) { @@ -102,21 +95,12 @@ export class SCMRepositoriesViewPane extends ViewPane { })); } + this.onDidChangeRepositories(); this.updateListSelection(); } - private onDidAddRepository(repository: ISCMRepository): void { - this.list.splice(this.list.length, 0, [repository]); - this.updateBodySize(); - } - - private onDidRemoveRepository(repository: ISCMRepository): void { - const index = this.list.indexOf(repository); - - if (index > -1) { - this.list.splice(index, 1); - } - + private onDidChangeRepositories(): void { + this.list.splice(0, this.list.length, this.scmViewService.repositories); this.updateBodySize(); } @@ -188,6 +172,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this.list.setSelection(selection); if (selection.length > 0) { + this.list.setAnchor(selection[0]); this.list.setFocus([selection[0]]); } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index df4814bb25f..79cce93a380 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2383,7 +2383,7 @@ export class SCMViewPane extends ViewPane { } } -export const scmProviderSeparatorBorderColor = registerColor('scm.providerBorder', { dark: '#454545', light: '#C8C8C8', hc: contrastBorder }, localize('scm.providerBorder', "SCM Provider separator border.")); +export const scmProviderSeparatorBorderColor = registerColor('scm.providerBorder', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, localize('scm.providerBorder', "SCM Provider separator border.")); registerThemingParticipant((theme, collector) => { const inputBackgroundColor = theme.getColor(inputBackground); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 78ac23a1588..7bf788fa113 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -11,11 +11,24 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { debounce } from 'vs/base/common/decorators'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { compareFileNames } from 'vs/base/common/comparers'; +import { basename } from 'vs/base/common/resources'; +import { binarySearch } from 'vs/base/common/arrays'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; } +function getRepositoryName(workspaceContextService: IWorkspaceContextService, repository: ISCMRepository): string { + if (!repository.provider.rootUri) { + return repository.provider.label; + } + + const folder = workspaceContextService.getWorkspaceFolder(repository.provider.rootUri); + return folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); +} + export interface ISCMViewServiceState { readonly all: string[]; readonly visible: number[]; @@ -32,6 +45,15 @@ export class SCMViewService implements ISCMViewService { private previousState: ISCMViewServiceState | undefined; private disposables = new DisposableStore(); + private _repositories: ISCMRepository[] = []; + + get repositories(): ISCMRepository[] { + return this._repositories; + } + + private _onDidChangeRepositories = new Emitter(); + readonly onDidChangeRepositories = this._onDidChangeRepositories.event; + private _visibleRepositoriesSet = new Set(); private _visibleRepositories: ISCMRepository[] = []; @@ -60,7 +82,7 @@ export class SCMViewService implements ISCMViewService { return; } - this._visibleRepositories = visibleRepositories; + this._visibleRepositories = visibleRepositories.sort(this._compareRepositories); this._visibleRepositoriesSet = set; this._onDidSetVisibleRepositories.fire({ added, removed }); @@ -69,7 +91,6 @@ export class SCMViewService implements ISCMViewService { } } - private _onDidChangeRepositories = new Emitter(); private _onDidSetVisibleRepositories = new Emitter(); readonly onDidChangeVisibleRepositories = Event.any( this._onDidSetVisibleRepositories.event, @@ -96,27 +117,36 @@ export class SCMViewService implements ISCMViewService { private _onDidFocusRepository = new Emitter(); readonly onDidFocusRepository = this._onDidFocusRepository.event; + private _compareRepositories: (op1: ISCMRepository, op2: ISCMRepository) => number; + constructor( @ISCMService private readonly scmService: ISCMService, @IInstantiationService instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService ) { this.menus = instantiationService.createInstance(SCMMenus); + this._compareRepositories = (op1: ISCMRepository, op2: ISCMRepository): number => { + const name1 = getRepositoryName(workspaceContextService, op1); + const name2 = getRepositoryName(workspaceContextService, op2); + + return compareFileNames(name1, name2); + }; + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - for (const repository of scmService.repositories) { - this.onDidAddRepository(repository); - } - try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); - this.eventuallyFinishLoading(); } catch { // noop } + for (const repository of scmService.repositories) { + this.onDidAddRepository(repository); + } + storageService.onWillSaveState(this.onWillSaveState, this, this.disposables); } @@ -125,6 +155,7 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } + this.insertRepository(this._repositories, repository); let removed: Iterable = Iterable.empty(); if (this.previousState) { @@ -138,8 +169,8 @@ export class SCMViewService implements ISCMViewService { } } - this._visibleRepositories = [...this.scmService.repositories]; this._visibleRepositoriesSet = new Set(this.scmService.repositories); + this._visibleRepositories = [...this.scmService.repositories.sort(this._compareRepositories)]; this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() }); this.finishLoading(); return; @@ -151,6 +182,7 @@ export class SCMViewService implements ISCMViewService { if (this._visibleRepositories.length === 0) { // should make it visible, until other repos come along this.provisionalVisibleRepository = repository; } else { + this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); return; } } else { @@ -163,8 +195,8 @@ export class SCMViewService implements ISCMViewService { } } - this._visibleRepositories.push(repository); this._visibleRepositoriesSet.add(repository); + this.insertRepository(this._visibleRepositories, repository); this._onDidChangeRepositories.fire({ added: [repository], removed }); if (!this._focusedRepository) { @@ -177,22 +209,29 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - const index = this._visibleRepositories.indexOf(repository); + let added: Iterable = Iterable.empty(); - if (index > -1) { - let added: Iterable = Iterable.empty(); + const repositoriesIndex = this._repositories.indexOf(repository); + const visibleRepositoriesIndex = this._visibleRepositories.indexOf(repository); - this._visibleRepositories.splice(index, 1); + if (repositoriesIndex > -1) { + this._repositories.splice(repositoriesIndex, 1); + } + + if (visibleRepositoriesIndex > -1) { + this._visibleRepositories.splice(visibleRepositoriesIndex, 1); this._visibleRepositoriesSet.delete(repository); - if (this._visibleRepositories.length === 0 && this.scmService.repositories.length > 0) { - const first = this.scmService.repositories[0]; + if (this._repositories.length > 0 && this._visibleRepositories.length === 0) { + const first = this._repositories[0]; this._visibleRepositories.push(first); this._visibleRepositoriesSet.add(first); added = [first]; } + } + if (repositoriesIndex > -1 || visibleRepositoriesIndex > -1) { this._onDidChangeRepositories.fire({ added, removed: [repository] }); } @@ -235,19 +274,26 @@ export class SCMViewService implements ISCMViewService { this._onDidFocusRepository.fire(repository); } + private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { + const index = binarySearch(repositories, repository, this._compareRepositories); + if (index < 0) { + repositories.splice(~index, 0, repository); + } + } + private onWillSaveState(): void { if (!this.didFinishLoading) { // don't remember state, if the workbench didn't really finish loading return; } - const all = this.scmService.repositories.map(r => getProviderStorageKey(r.provider)); + const all = this.repositories.map(r => getProviderStorageKey(r.provider)); const visible = this.visibleRepositories.map(r => all.indexOf(getProviderStorageKey(r.provider))); const raw = JSON.stringify({ all, visible }); this.storageService.store('scm:view:visibleRepositories', raw, StorageScope.WORKSPACE, StorageTarget.MACHINE); } - @debounce(2000) + @debounce(5000) private eventuallyFinishLoading(): void { this.finishLoading(); } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 1056cbab75a..c46af5c54b5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -180,6 +180,9 @@ export interface ISCMViewService { readonly menus: ISCMMenus; + repositories: ISCMRepository[]; + readonly onDidChangeRepositories: Event; + visibleRepositories: ISCMRepository[]; readonly onDidChangeVisibleRepositories: Event; diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 3914df6ad43..f9e5c40fa0d 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -257,7 +257,8 @@ } /* Adjusts spacing in high contrast mode so that actions are vertically centered */ -.monaco-workbench.hc-black .search-view .monaco-list .monaco-list-row .monaco-action-bar .action-label { +.monaco-workbench.hc-black .search-view .monaco-list .monaco-list-row .monaco-action-bar .action-label, +.monaco-workbench.hc-light .search-view .monaco-list .monaco-list-row .monaco-action-bar .action-label { margin-top: 2px; } @@ -284,7 +285,9 @@ } .monaco-workbench.hc-black .search-view .replaceMatch, -.monaco-workbench.hc-black .search-view .findInFileMatch { +.monaco-workbench.hc-black .search-view .findInFileMatch, +.monaco-workbench.hc-light .search-view .replaceMatch, +.monaco-workbench.hc-light .search-view .findInFileMatch { background: none !important; box-sizing: border-box; } @@ -293,7 +296,10 @@ .monaco-workbench.hc-black .search-view .foldermatch, .monaco-workbench.hc-black .search-view .filematch, -.monaco-workbench.hc-black .search-view .linematch { +.monaco-workbench.hc-black .search-view .linematch, +.monaco-workbench.hc-light .search-view .foldermatch, +.monaco-workbench.hc-light .search-view .filematch, +.monaco-workbench.hc-light .search-view .linematch { line-height: 20px; } diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 6e275c21812..5e9bedd5609 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -5,7 +5,7 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { HistoryInputBox, IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; @@ -19,7 +19,7 @@ import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWi import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { attachCheckboxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { attachToggleStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; export interface IOptions { @@ -201,7 +201,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); } - private useSearchInEditorsBox!: Checkbox; + private useSearchInEditorsBox!: Toggle; override dispose(): void { super.dispose(); @@ -222,7 +222,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { } protected override renderSubcontrols(controlsDiv: HTMLDivElement): void { - this.useSearchInEditorsBox = this._register(new Checkbox({ + this.useSearchInEditorsBox = this._register(new Toggle({ icon: Codicon.book, title: nls.localize('onlySearchInOpenEditors', "Search only in Open Editors"), isChecked: false, @@ -233,7 +233,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { this.inputBox.focus(); } })); - this._register(attachCheckboxStyler(this.useSearchInEditorsBox, this.themeService)); + this._register(attachToggleStyler(this.useSearchInEditorsBox, this.themeService)); controlsDiv.appendChild(this.useSearchInEditorsBox.domNode); super.renderSubcontrols(controlsDiv); } @@ -253,7 +253,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget { super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); } - private useExcludesAndIgnoreFilesBox!: Checkbox; + private useExcludesAndIgnoreFilesBox!: Toggle; override dispose(): void { super.dispose(); @@ -274,7 +274,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget { } protected override renderSubcontrols(controlsDiv: HTMLDivElement): void { - this.useExcludesAndIgnoreFilesBox = this._register(new Checkbox({ + this.useExcludesAndIgnoreFilesBox = this._register(new Toggle({ icon: Codicon.exclude, actionClassName: 'useExcludesAndIgnoreFiles', title: nls.localize('useExcludesAndIgnoreFilesDescription', "Use Exclude Settings and Ignore Files"), @@ -286,7 +286,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget { this.inputBox.focus(); } })); - this._register(attachCheckboxStyler(this.useExcludesAndIgnoreFilesBox, this.themeService)); + this._register(attachToggleStyler(this.useExcludesAndIgnoreFilesBox, this.themeService)); controlsDiv.appendChild(this.useExcludesAndIgnoreFilesBox.domNode); super.renderSubcontrols(controlsDiv); diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index b1fc4bd1e18..ba8ebda0f19 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -26,6 +26,7 @@ import { EditOperation, ISingleEditOperation } from 'vs/editor/common/core/editO import { ILabelService } from 'vs/platform/label/common/label'; import { dirname } from 'vs/base/common/resources'; import { Promises } from 'vs/base/common/async'; +import { SaveSourceRegistry } from 'vs/workbench/common/editor'; const REPLACE_PREVIEW = 'replacePreview'; @@ -91,6 +92,8 @@ export class ReplaceService implements IReplaceService { declare readonly _serviceBrand: undefined; + private static readonly REPLACE_SAVE_SOURCE = SaveSourceRegistry.registerSource('searchReplace.source', nls.localize('searchReplace.source', "Search and Replace")); + constructor( @ITextFileService private readonly textFileService: ITextFileService, @IEditorService private readonly editorService: IEditorService, @@ -106,7 +109,7 @@ export class ReplaceService implements IReplaceService { const edits = this.createEdits(arg, resource); await this.bulkEditorService.apply(edits, { progress }); - return Promises.settled(edits.map(async e => this.textFileService.files.get(e.resource)?.save())); + return Promises.settled(edits.map(async e => this.textFileService.files.get(e.resource)?.save({ source: ReplaceService.REPLACE_SAVE_SOURCE }))); } async openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index e1e4c010d2e..90411d0caec 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -78,6 +78,7 @@ import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurati import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ResourceListDnDHandler } from 'vs/workbench/browser/dnd'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; const $ = dom.$; @@ -1928,17 +1929,17 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = const diffInsertedOutlineColor = theme.getColor(diffInsertedOutline); if (diffInsertedOutlineColor) { - collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffInsertedOutlineColor}; }`); + collector.addRule(`.monaco-workbench .search-view .replaceMatch:not(:empty) { border: 1px ${isHighContrast(theme.type) ? 'dashed' : 'solid'} ${diffInsertedOutlineColor}; }`); } const diffRemovedOutlineColor = theme.getColor(diffRemovedOutline); if (diffRemovedOutlineColor) { - collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${diffRemovedOutlineColor}; }`); + collector.addRule(`.monaco-workbench .search-view .replace.findInFileMatch { border: 1px ${isHighContrast(theme.type) ? 'dashed' : 'solid'} ${diffRemovedOutlineColor}; }`); } const findMatchHighlightBorder = theme.getColor(editorFindMatchHighlightBorder); if (findMatchHighlightBorder) { - collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${theme.type === 'hc' ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`); + collector.addRule(`.monaco-workbench .search-view .findInFileMatch { border: 1px ${isHighContrast(theme.type) ? 'dashed' : 'solid'} ${findMatchHighlightBorder}; }`); } const outlineSelectionColor = theme.getColor(listActiveSelectionForeground); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 9746dab23b4..d9d0f908be9 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -31,7 +31,7 @@ import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/wo import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { isMacintosh } from 'vs/base/common/platform'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IViewsService } from 'vs/workbench/common/views'; import { searchReplaceAllIcon, searchHideReplaceIcon, searchShowContextIcon, searchShowReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { ToggleSearchEditorContextLinesCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; @@ -148,7 +148,7 @@ export class SearchWidget extends Widget { private readonly _onDidToggleContext = new Emitter(); readonly onDidToggleContext: Event = this._onDidToggleContext.event; - private showContextCheckbox!: Checkbox; + private showContextToggle!: Toggle; public contextLinesInput!: InputBox; constructor( @@ -355,12 +355,12 @@ export class SearchWidget extends Widget { this._register(this.searchInputFocusTracker.onDidBlur(() => this.searchInputBoxFocused.set(false))); - this.showContextCheckbox = new Checkbox({ + this.showContextToggle = new Toggle({ isChecked: false, title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keybindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId), this.keybindingService), icon: searchShowContextIcon }); - this._register(this.showContextCheckbox.onChange(() => this.onContextLinesChanged())); + this._register(this.showContextToggle.onChange(() => this.onContextLinesChanged())); if (options.showContextToggle) { this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); @@ -368,7 +368,7 @@ export class SearchWidget extends Widget { this.contextLinesInput.value = '' + (this.configurationService.getValue('search').searchEditor.defaultNumberOfContextLines ?? 1); this._register(this.contextLinesInput.onDidChange(() => this.onContextLinesChanged())); this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); - dom.append(searchInputContainer, this.showContextCheckbox.domNode); + dom.append(searchInputContainer, this.showContextToggle.domNode); } } @@ -385,9 +385,9 @@ export class SearchWidget extends Widget { public setContextLines(lines: number) { if (!this.contextLinesInput) { return; } if (lines === 0) { - this.showContextCheckbox.checked = false; + this.showContextToggle.checked = false; } else { - this.showContextCheckbox.checked = true; + this.showContextToggle.checked = true; this.contextLinesInput.value = '' + lines; } } @@ -640,18 +640,18 @@ export class SearchWidget extends Widget { } getContextLines() { - return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0; + return this.showContextToggle.checked ? +this.contextLinesInput.value : 0; } modifyContextLines(increase: boolean) { const current = +this.contextLinesInput.value; const modified = current + (increase ? 1 : -1); - this.showContextCheckbox.checked = modified !== 0; + this.showContextToggle.checked = modified !== 0; this.contextLinesInput.value = '' + modified; } toggleContextLines() { - this.showContextCheckbox.checked = !this.showContextCheckbox.checked; + this.showContextToggle.checked = !this.showContextToggle.checked; this.onContextLinesChanged(); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 67292a26cec..f67dd7c748c 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -61,6 +61,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchMessage'; import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { UnusualLineTerminatorsDetector } from 'vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -724,11 +725,11 @@ registerThemingParticipant((theme, collector) => { const findMatchHighlightBorder = theme.getColor(searchEditorFindMatchBorder); if (findMatchHighlightBorder) { - collector.addRule(`.monaco-editor .${SearchEditorFindMatchClass} { border: 1px ${theme.type === 'hc' ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); + collector.addRule(`.monaco-editor .${SearchEditorFindMatchClass} { border: 1px ${isHighContrast(theme.type) ? 'dotted' : 'solid'} ${findMatchHighlightBorder}; box-sizing: border-box; }`); } }); -export const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border.")); +export const searchEditorTextInputBorder = registerColor('searchEditor.textInputBorder', { dark: inputBorder, light: inputBorder, hcDark: inputBorder, hcLight: inputBorder }, localize('textInputBoxBorder', "Search editor text input box border.")); function findNextRange(matchRanges: Range[], currentPosition: Position) { for (const matchRange of matchRanges) { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 00c9ce82e50..5cf82a06537 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -24,7 +24,7 @@ import { defaultSearchConfig, parseSavedSearchEditor, serializeSearchConfigurati import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopySaveEvent, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ISearchComplete, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; @@ -75,6 +75,9 @@ export class SearchEditorInput extends EditorInput { private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave: Event = this._onDidSave.event; + private oldDecorationsIDs: string[] = []; get resource() { @@ -118,6 +121,7 @@ export class SearchEditorInput extends EditorInput { readonly capabilities = input.hasCapability(EditorInputCapabilities.Untitled) ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; + readonly onDidSave = input.onDidSave; isDirty(): boolean { return input.isDirty(); } backup(token: CancellationToken): Promise { return input.backup(token); } save(options?: ISaveOptions): Promise { return input.save(0, options).then(editor => !!editor); } @@ -133,6 +137,7 @@ export class SearchEditorInput extends EditorInput { if (this.backingUri) { await this.textFileService.write(this.backingUri, await this.serializeForDisk(), options); this.setDirty(false); + this._onDidSave.fire({ reason: options?.reason, source: options?.source }); return this; } else { return this.saveAs(group, options); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index fe8032b4490..70c269e4a69 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1585,8 +1585,13 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer let resolverData: Map | undefined; - async function quickResolve(that: AbstractTaskService, identifier: string | TaskIdentifier) { + async function quickResolve(that: AbstractTaskService, uri: URI | string, identifier: string | TaskIdentifier) { const foundTasks = await that._findWorkspaceTasks((task: Task | ConfiguringTask): boolean => { + const taskUri = ((ConfiguringTask.is(task) || CustomTask.is(task)) ? task._source.config.workspaceFolder?.uri : undefined); + const originalUri = (typeof uri === 'string' ? uri : uri.toString()); + if (taskUri?.toString() !== originalUri) { + return false; + } if (Types.isString(identifier)) { return ((task._label === identifier) || (task.configurationProperties.identifier === identifier)); } else { @@ -1649,7 +1654,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return undefined; } if ((resolverData === undefined) && (grouped === undefined)) { - return (await quickResolve(this, identifier)) ?? fullResolve(this, uri, identifier); + return (await quickResolve(this, uri, identifier)) ?? fullResolve(this, uri, identifier); } else { return fullResolve(this, uri, identifier); } @@ -1695,7 +1700,10 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer const taskFolder = task.getWorkspaceFolder(); const taskIdentifier = task.configurationProperties.identifier; // Since we save before running tasks, the task may have changed as part of the save. - taskToRun = ((taskFolder && taskIdentifier) ? await this.getTask(taskFolder, taskIdentifier) : task) ?? task; + // However, if the TaskRunSource is not User, then we shouldn't try to fetch the task again + // since this can cause a new'd task to get overwritten with a provided task. + taskToRun = ((taskFolder && taskIdentifier && (runSource === TaskRunSource.User)) + ? await this.getTask(taskFolder, taskIdentifier) : task) ?? task; } await ProblemMatcherRegistry.onReady(); let executeResult = this.getTaskSystem().run(taskToRun, resolver); diff --git a/src/vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability.ts b/src/vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability.ts deleted file mode 100644 index ff0d189d347..00000000000 --- a/src/vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability.ts +++ /dev/null @@ -1,238 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { timeout } from 'vs/base/common/async'; -import { Emitter } from 'vs/base/common/event'; -import { ILogService } from 'vs/platform/log/common/log'; -import { ICommandDetectionCapability, TerminalCapability, ITerminalCommand } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { IBuffer, IDisposable, IMarker, Terminal } from 'xterm'; - -export interface ICurrentPartialCommand { - previousCommandMarker?: IMarker; - - promptStartMarker?: IMarker; - - commandStartMarker?: IMarker; - commandStartX?: number; - - commandLines?: IMarker; - - commandExecutedMarker?: IMarker; - commandExecutedX?: number; - - commandFinishedMarker?: IMarker; - - currentContinuationMarker?: IMarker; - continuations?: { marker: IMarker; end: number }[]; - - command?: string; -} - -export class CommandDetectionCapability implements ICommandDetectionCapability { - readonly type = TerminalCapability.CommandDetection; - - protected _commands: ITerminalCommand[] = []; - private _exitCode: number | undefined; - private _cwd: string | undefined; - private _currentCommand: ICurrentPartialCommand = {}; - private _isWindowsPty: boolean = false; - private _onCursorMoveListener?: IDisposable; - private _commandMarkers: IMarker[] = []; - - get commands(): readonly ITerminalCommand[] { return this._commands; } - - private readonly _onCommandStarted = new Emitter(); - readonly onCommandStarted = this._onCommandStarted.event; - private readonly _onCommandFinished = new Emitter(); - readonly onCommandFinished = this._onCommandFinished.event; - - constructor( - private readonly _terminal: Terminal, - @ILogService private readonly _logService: ILogService - ) { } - - setCwd(value: string) { - this._cwd = value; - } - - setIsWindowsPty(value: boolean) { - this._isWindowsPty = value; - } - - getCwdForLine(line: number): string | undefined { - // TODO: It would be more reliable to take the closest cwd above the line if it isn't found for the line - // TODO: Use a reverse for loop to find the line to avoid creating another array - const reversed = [...this._commands].reverse(); - return reversed.find(c => c.marker!.line <= line - 1)?.cwd; - } - - handlePromptStart(): void { - this._currentCommand.promptStartMarker = this._terminal.registerMarker(0); - this._logService.debug('CommandDetectionCapability#handlePromptStart', this._terminal.buffer.active.cursorX, this._currentCommand.promptStartMarker?.line); - } - - handleContinuationStart(): void { - this._currentCommand.currentContinuationMarker = this._terminal.registerMarker(0); - this._logService.debug('CommandDetectionCapability#handleContinuationStart', this._currentCommand.currentContinuationMarker); - } - - handleContinuationEnd(): void { - if (!this._currentCommand.currentContinuationMarker) { - this._logService.warn('CommandDetectionCapability#handleContinuationEnd Received continuation end without start'); - return; - } - if (!this._currentCommand.continuations) { - this._currentCommand.continuations = []; - } - this._currentCommand.continuations.push({ - marker: this._currentCommand.currentContinuationMarker, - end: this._terminal.buffer.active.cursorX - }); - this._currentCommand.currentContinuationMarker = undefined; - this._logService.debug('CommandDetectionCapability#handleContinuationEnd', this._currentCommand.continuations[this._currentCommand.continuations.length - 1]); - } - - handleCommandStart(): void { - this._currentCommand.commandStartX = this._terminal.buffer.active.cursorX; - - // On Windows track all cursor movements after the command start sequence - if (this._isWindowsPty) { - this._commandMarkers.length = 0; - this._onCursorMoveListener = this._terminal.onCursorMove(() => { - if (this._commandMarkers.length === 0 || this._commandMarkers[this._commandMarkers.length - 1].line !== this._terminal.buffer.active.cursorY) { - const marker = this._terminal.registerMarker(0); - if (marker) { - this._commandMarkers.push(marker); - } - } - }); - // HACK: Fire command started on the following frame on Windows to allow the cursor - // position to update as conpty often prints the sequence on a different line to the - // actual line the command started on. - timeout(0).then(() => { - this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); - this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker } as ITerminalCommand); - }); - } else { - this._currentCommand.commandStartMarker = this._terminal.registerMarker(0); - this._onCommandStarted.fire({ marker: this._currentCommand.commandStartMarker } as ITerminalCommand); - } - this._logService.debug('CommandDetectionCapability#handleCommandStart', this._currentCommand.commandStartX, this._currentCommand.commandStartMarker?.line); - } - - handleCommandExecuted(): void { - // On Windows, use the gathered cursor move markers to correct the command start and - // executed markers - if (this._isWindowsPty) { - this._onCursorMoveListener?.dispose(); - this._onCursorMoveListener = undefined; - } - - this._currentCommand.commandExecutedMarker = this._terminal.registerMarker(0); - this._currentCommand.commandExecutedX = this._terminal.buffer.active.cursorX; - this._logService.debug('CommandDetectionCapability#handleCommandExecuted', this._currentCommand.commandExecutedX, this._currentCommand.commandExecutedMarker?.line); - - // Don't get the command on Windows, rely on the command line sequence for this - if (this._isWindowsPty) { - return; - } - - // Sanity check optional props - if (!this._currentCommand.commandStartMarker || !this._currentCommand.commandExecutedMarker || !this._currentCommand.commandStartX) { - return; - } - - // Calculate the command - this._currentCommand.command = this._terminal.buffer.active.getLine(this._currentCommand.commandStartMarker.line)?.translateToString(true, this._currentCommand.commandStartX); - let y = this._currentCommand.commandStartMarker.line + 1; - const commandExecutedLine = this._currentCommand.commandExecutedMarker.line; - for (; y < commandExecutedLine; y++) { - const line = this._terminal.buffer.active.getLine(y); - if (line) { - const continuation = this._currentCommand.continuations?.find(e => e.marker.line === y); - if (continuation) { - this._currentCommand.command += '\n'; - } - const startColumn = continuation?.end ?? 0; - this._currentCommand.command += line.translateToString(true, startColumn); - } - } - if (y === commandExecutedLine) { - this._currentCommand.command += this._terminal.buffer.active.getLine(commandExecutedLine)?.translateToString(true, undefined, this._currentCommand.commandExecutedX) || ''; - } - } - - handleCommandFinished(exitCode: number | undefined): void { - // On Windows, use the gathered cursor move markers to correct the command start and - // executed markers. This is done on command finished just in case command executed never - // happens (for example PSReadLine tab completion) - if (this._isWindowsPty) { - this._commandMarkers = this._commandMarkers.sort((a, b) => a.line - b.line); - this._currentCommand.commandStartMarker = this._commandMarkers[0]; - this._currentCommand.commandExecutedMarker = this._commandMarkers[this._commandMarkers.length - 1]; - } - - this._currentCommand.commandFinishedMarker = this._terminal.registerMarker(0); - const command = this._currentCommand.command; - this._logService.debug('CommandDetectionCapability#handleCommandFinished', this._terminal.buffer.active.cursorX, this._currentCommand.commandFinishedMarker?.line, this._currentCommand.command, this._currentCommand); - this._exitCode = exitCode; - - // HACK: Handle a special case on some versions of bash where identical commands get merged - // in the output of `history`, this detects that case and sets the exit code to the the last - // command's exit code. This covered the majority of cases but will fail if the same command - // runs with a different exit code, that will need a more robust fix where we send the - // command ID and exit code over to the capability to adjust there. - if (this._exitCode === undefined) { - const lastCommand = this.commands.length > 0 ? this.commands[this.commands.length - 1] : undefined; - if (command && command.length > 0 && lastCommand?.command === command) { - this._exitCode = lastCommand.exitCode; - } - } - - if (this._currentCommand.commandStartMarker === undefined || !this._terminal.buffer.active) { - return; - } - - if (command !== undefined && !command.startsWith('\\')) { - const buffer = this._terminal.buffer.active; - const clonedPartialCommand = { ...this._currentCommand }; - const timestamp = Date.now(); - const newCommand = { - command, - marker: this._currentCommand.commandStartMarker, - endMarker: this._currentCommand.commandFinishedMarker, - timestamp, - cwd: this._cwd, - exitCode: this._exitCode, - hasOutput: !!(this._currentCommand.commandExecutedMarker && this._currentCommand.commandFinishedMarker && this._currentCommand.commandExecutedMarker?.line < this._currentCommand.commandFinishedMarker!.line), - getOutput: () => getOutputForCommand(clonedPartialCommand, buffer) - }; - this._commands.push(newCommand); - this._logService.debug('CommandDetectionCapability#onCommandFinished', newCommand); - this._onCommandFinished.fire(newCommand); - } - this._currentCommand.previousCommandMarker = this._currentCommand.commandStartMarker; - this._currentCommand = {}; - } - - setCommandLine(commandLine: string) { - this._logService.debug('CommandDetectionCapability#setCommandLine', commandLine); - this._currentCommand.command = commandLine; - } -} - -function getOutputForCommand(command: ICurrentPartialCommand, buffer: IBuffer): string | undefined { - const startLine = command.commandExecutedMarker!.line; - const endLine = command.commandFinishedMarker!.line; - - if (startLine === endLine) { - return undefined; - } - let output = ''; - for (let i = startLine; i < endLine; i++) { - output += buffer.getLine(i)?.translateToString() + '\n'; - } - return output === '' ? undefined : output; -} diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index 1a8b6eae8b4..202ba173cc0 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -7,7 +7,7 @@ import type { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPos import { IRange } from 'vs/editor/common/core/range'; import { OperatingSystem } from 'vs/base/common/platform'; import { IPath, posix, win32 } from 'vs/base/common/path'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; /** * Converts a possibly wrapped link's range (comprised of string indices) into a buffer range that plays nicely with xterm.js diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index 5e393d6e20d..15d0dd806e6 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -29,7 +29,7 @@ import { ITerminalExternalLinkProvider, TerminalLinkQuickPickEvent } from 'vs/wo import { ILinkHoverTargetOptions, TerminalHover } from 'vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ITerminalConfiguration, ITerminalProcessManager, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { IHoverAction } from 'vs/workbench/services/hover/browser/hover'; import type { ILink, ILinkProvider, IViewportRange, Terminal } from 'xterm'; @@ -303,11 +303,13 @@ export class TerminalLinkManager extends DisposableStore { } } - let fallbackLabel: string; - if (this._tunnelService.canTunnel(URI.parse(uri))) { - fallbackLabel = nls.localize('followForwardedLink', "Follow link using forwarded port"); - } else { - fallbackLabel = nls.localize('followLink', "Follow link"); + let fallbackLabel = nls.localize('followLink', "Follow link"); + try { + if (this._tunnelService.canTunnel(URI.parse(uri))) { + fallbackLabel = nls.localize('followForwardedLink', "Follow link using forwarded port"); + } + } catch { + // No-op, already set to fallback } const markdown = new MarkdownString('', true); @@ -329,7 +331,7 @@ export class TerminalLinkManager extends DisposableStore { uri = nls.localize('followLinkUrl', 'Link'); } - return markdown.appendMarkdown(`[${label}](${uri}) (${clickLabel})`); + return markdown.appendLink(uri, label).appendMarkdown(` (${clickLabel})`); } private get _osPath(): IPath { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts index 2792911d0ef..22598d4aba7 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners.ts @@ -17,7 +17,7 @@ import { ITerminalLinkOpener, ITerminalSimpleLink } from 'vs/workbench/contrib/t import { osPathModule, updateLinkWithRelativeCwd } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { ILineColumnInfo } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { getLocalLinkRegex, lineAndColumnClause, lineAndColumnClauseGroupCount, unixLineAndColumnMatchIndex, winLineAndColumnMatchIndex } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IHostService } from 'vs/workbench/services/host/browser/host'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts index 7406f47eb55..936af397072 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts @@ -9,7 +9,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITerminalLinkDetector, ITerminalSimpleLink, ResolvedLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links'; import { convertLinkRangeToBuffer, getXtermLineContent, osPathModule, updateLinkWithRelativeCwd } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IBufferLine, Terminal } from 'xterm'; const enum Constants { diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh index 211b4ef32fa..072262d6c87 100755 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh @@ -59,8 +59,8 @@ command_complete() { update_prompt() { PRIOR_PROMPT="$PS1" IN_COMMAND_EXECUTION="" - PS1="$(prompt_start)$PREFIX$PS1$(prompt_end)" - PS2="$(continuation_start)$PS2$(continuation_end)" + PS1="\[$(prompt_start)\]$PREFIX$PS1\[$(prompt_end)\]" + PS2="\[$(continuation_start)\]$PS2\[$(continuation_end)\]" } precmd() { @@ -83,14 +83,32 @@ preexec() { update_prompt prompt_cmd_original() { STATUS="$?" - $ORIGINAL_PROMPT_COMMAND + if [[ "$ORIGINAL_PROMPT_COMMAND" =~ .+\;.+ ]]; then + IFS=';' + else + IFS=' ' + fi + read -ra ADDR <<<"$ORIGINAL_PROMPT_COMMAND" + for ((i = 0; i < ${#ADDR[@]}; i++)); do + eval ${ADDR[i]} + done + IFS='' precmd } + prompt_cmd() { STATUS="$?" precmd } -ORIGINAL_PROMPT_COMMAND=$PROMPT_COMMAND + +if [[ "$PROMPT_COMMAND" =~ (.+\;.+) ]]; then + # item1;item2... + ORIGINAL_PROMPT_COMMAND="$PROMPT_COMMAND" +else + # (item1, item2...) + ORIGINAL_PROMPT_COMMAND=${PROMPT_COMMAND[@]} +fi + if [[ -n "$ORIGINAL_PROMPT_COMMAND" && "$ORIGINAL_PROMPT_COMMAND" != "prompt_cmd" ]]; then PROMPT_COMMAND=prompt_cmd_original else diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 index 9ba614a8ab3..9405b77223f 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1 @@ -23,23 +23,26 @@ function Global:__VSCode-Get-LastExitCode { function Global:Prompt() { $LastExitCode = $(__VSCode-Get-LastExitCode); $LastHistoryEntry = $(Get-History -Count 1) - if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) { - # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command) - $Result = "`e]633;E`a" - $Result += "`e]633;D`a" - } else { - # Command finished command line - # OSC 633 ; A ; ST - $Result = "`e]633;E;" - # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed - # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter - # to only be composed of _printable_ characters as per the spec. - $CommandLine = $LastHistoryEntry.CommandLine ?? "" - $Result += $CommandLine.Replace("`n", "").Replace(";", "") - $Result += "`a" - # Command finished exit code - # OSC 633 ; D [; ] ST - $Result += "`e]633;D;$LastExitCode`a" + # Skip finishing the command if the first command has not yet started + if ($Global:__LastHistoryId -ne -1) { + if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) { + # Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command) + $Result = "`e]633;E`a" + $Result += "`e]633;D`a" + } else { + # Command finished command line + # OSC 633 ; A ; ST + $Result = "`e]633;E;" + # Sanitize the command line to ensure it can get transferred to the terminal and can be parsed + # correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter + # to only be composed of _printable_ characters as per the spec. + $CommandLine = $LastHistoryEntry.CommandLine ?? "" + $Result += $CommandLine.Replace("`n", "").Replace(";", "") + $Result += "`a" + # Command finished exit code + # OSC 633 ; D [; ] ST + $Result += "`e]633;D;$LastExitCode`a" + } } # Prompt started # OSC 633 ; A ST @@ -55,13 +58,16 @@ function Global:Prompt() { return $Result } -# TODO: Gracefully fallback when PSReadLine is not loaded -$__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine -function Global:PSConsoleHostReadLine { - $tmp = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() - # Write command executed sequence directly to Console to avoid the new line from Write-Host - [Console]::Write("`e]633;C`a") - $tmp +# Only send the command executed sequence when PSReadLine is loaded, if not shell integration should +# still work thanks to the command line sequence +if (Get-Module -Name PSReadLine) { + $__VSCodeOriginalPSConsoleHostReadLine = $function:PSConsoleHostReadLine + function Global:PSConsoleHostReadLine { + $tmp = $__VSCodeOriginalPSConsoleHostReadLine.Invoke() + # Write command executed sequence directly to Console to avoid the new line from Write-Host + [Console]::Write("`e]633;C`a") + $tmp + } } # Set IsWindows property diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh index fe21ba4994f..3113138f1ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh @@ -2,9 +2,19 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. # --------------------------------------------------------------------------------------------- - autoload -Uz add-zsh-hook +if [ -f ~/.zshenv ]; then + . ~/.zshenv +fi +if [[ -o "login" && -f ~/.zprofile ]]; then + . ~/.zprofile +fi +if [ -f ~/.zshrc ]; then + . ~/.zshrc +fi +unset ZDOTDIR # ensure ~/.zlogout runs as expected + IN_COMMAND_EXECUTION="1" LAST_HISTORY_ID=0 @@ -32,6 +42,14 @@ continuation_end() { printf "\033]633;G\007" } +right_prompt_start() { + printf "\033]633;H\007" +} + +right_prompt_end() { + printf "\033]633;I\007" +} + command_complete() { local HISTORY_ID=$(history | tail -n1 | awk '{print $1;}') if [[ "$HISTORY_ID" == "$LAST_HISTORY_ID" ]]; then @@ -46,8 +64,12 @@ command_complete() { update_prompt() { PRIOR_PROMPT="$PS1" IN_COMMAND_EXECUTION="" - PS1="$(prompt_start)$PREFIX$PS1$(prompt_end)" - PS2="$(continuation_start)$PS2$(continuation_end)" + PS1="%{$(prompt_start)%}$PREFIX$PS1%{$(prompt_end)%}" + PS2="%{$(continuation_start)%}$PS2%{$(continuation_end)%}" + if [ -n "$RPROMPT" ]; then + PRIOR_RPROMPT="$RPROMPT" + RPROMPT="%{$(right_prompt_start)%}$RPROMPT%{$(right_prompt_end)%}" + fi } precmd() { @@ -68,6 +90,9 @@ precmd() { preexec() { PS1="$PRIOR_PROMPT" + if [ -n "$RPROMPT" ]; then + RPROMPT="$PRIOR_RPROMPT" + fi IN_COMMAND_EXECUTION="1" command_output_start } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 7014f0152f9..de5704e9673 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -70,6 +70,12 @@ z-index: 30; } +.monaco-workbench .editor-instance .xterm-decoration-overview-ruler, +.monaco-workbench .pane-body.integrated-terminal .xterm-decoration-overview-ruler { + z-index: 31; /* Must be higher than .xterm-viewport */ + pointer-events: none; +} + .monaco-workbench .editor-instance .xterm-screen, .monaco-workbench .pane-body.integrated-terminal .xterm-screen { z-index: 31; @@ -93,10 +99,15 @@ display: block; } -.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .xterm, -.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .xterm { - padding-left: 20px; +.monaco-workbench .editor-instance .xterm { + padding-left: 20px !important; } + +.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .xterm, +.integrated-terminal.shell-integration .xterm { + padding-left: 20px !important; +} + .monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm, .monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .xterm { padding-right: 20px; @@ -108,10 +119,6 @@ position: relative; } -.monaco-workbench .xterm .hoverHighlight { - pointer-events: none; -} - .monaco-workbench .editor-instance .terminal-wrapper > div, .monaco-workbench .pane-body.integrated-terminal .terminal-wrapper > div { height: 100%; @@ -162,15 +169,6 @@ border-top-style: solid; } -.monaco-workbench .pane-body.integrated-terminal.enable-ligatures { - font-variant-ligatures: normal; -} - - -.monaco-workbench .pane-body.integrated-terminal.disable-bold .xterm-bold { - font-weight: normal !important; -} - /* Use the default cursor when alt is active to help with clicking to move cursor */ .monaco-workbench .pane-body.integrated-terminal .terminal-groups-container.alt-active .xterm { cursor: default; @@ -192,7 +190,9 @@ } .monaco-workbench.hc-black .pane-body.integrated-terminal .xterm.focus::before, -.monaco-workbench.hc-black .pane-body.integrated-terminal .xterm:focus::before { +.monaco-workbench.hc-black .pane-body.integrated-terminal .xterm:focus::before, +.monaco-workbench.hc-light .pane-body.integrated-terminal .xterm:focus::before, +.monaco-workbench.hc-light .pane-body.integrated-terminal .xterm.focus::before { display: block; content: ""; border: 1px solid; @@ -205,7 +205,9 @@ } .monaco-workbench.hc-black .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm.focus::before, -.monaco-workbench.hc-black .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm:focus::before { +.monaco-workbench.hc-black .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm:focus::before, +.monaco-workbench.hc-light .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm.focus::before, +.monaco-workbench.hc-light .pane-body.integrated-terminal .monaco-split-view2.horizontal .split-view-view:not(:only-child) .xterm:focus::before { right: 0; } @@ -425,3 +427,9 @@ .terminal-command-decoration.default { pointer-events: none; } + +.terminal-scroll-highlight { + left: 0; + right: 0; + border: 1px solid #ffffff; +} diff --git a/src/vs/workbench/contrib/terminal/browser/media/widgets.css b/src/vs/workbench/contrib/terminal/browser/media/widgets.css index 58545a1df0a..a7a58e9bf64 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/widgets.css +++ b/src/vs/workbench/contrib/terminal/browser/media/widgets.css @@ -5,22 +5,13 @@ .monaco-workbench .terminal-widget-container { position: absolute; - left: 10px; - bottom: 2px; - right: 10px; + left: 0; + bottom: 0; + right: 0; top: 0; overflow: visible; } -.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .terminal-widget-container, -.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .terminal-widget-container { - left: 20px; -} -.monaco-workbench .editor-instance .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .terminal-widget-container, -.monaco-workbench .pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:last-child .terminal-widget-container { - right: 20px; -} - .monaco-workbench .terminal-overlay-widget { position: absolute; left: 0; diff --git a/src/vs/workbench/contrib/terminal/browser/media/xterm.css b/src/vs/workbench/contrib/terminal/browser/media/xterm.css index c2f08b5268f..30ac0800d21 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/xterm.css +++ b/src/vs/workbench/contrib/terminal/browser/media/xterm.css @@ -182,3 +182,10 @@ z-index: 6; position: absolute; } + +.xterm-decoration-overview-ruler { + z-index: 7; + position: absolute; + top: 0; + right: 0; +} diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index 62b17bf8ea8..cc7b5d1b6d5 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -9,7 +9,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; -import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; +import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; import { RemoteTerminalChannelClient } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -22,6 +22,8 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { readonly onDidChangeProperty = this._onDidChangeProperty.event; private readonly _onProcessExit = this._register(new Emitter()); readonly onProcessExit = this._onProcessExit.event; + private readonly _onRestoreCommands = this._register(new Emitter()); + readonly onRestoreCommands = this._onRestoreCommands.event; private _startBarrier: Barrier; @@ -178,6 +180,10 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { this._inReplay = false; } + if (e.commands) { + this._onRestoreCommands.fire(e.commands); + } + // remove size override this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index 716cb74198e..dbbcea77cf9 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -14,8 +14,8 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { Registry } from 'vs/platform/registry/common/platform'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalEnvironment, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalIcon, TerminalSettingId, TitleEventSource } from 'vs/platform/terminal/common/terminal'; -import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; +import { IShellLaunchConfig, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalEnvironment, ITerminalProcessOptions, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalIcon, TerminalSettingId, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessDetails, ISerializedCommand } from 'vs/platform/terminal/common/terminalProcess'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { BaseTerminalBackend } from 'vs/workbench/contrib/terminal/browser/baseTerminalBackend'; @@ -52,6 +52,8 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack private readonly _onDidRequestDetach = this._register(new Emitter<{ requestId: number; workspaceId: string; instanceId: number }>()); readonly onDidRequestDetach = this._onDidRequestDetach.event; + private readonly _onRestoreCommands = this._register(new Emitter<{ id: number; commands: ISerializedCommand[] }>()); + readonly onRestoreCommands = this._onRestoreCommands.event; constructor( readonly remoteAuthority: string | undefined, @@ -70,7 +72,12 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack super(_remoteTerminalChannel, logService, notificationService, _historyService, configurationResolverService, workspaceContextService); this._remoteTerminalChannel.onProcessData(e => this._ptys.get(e.id)?.handleData(e.event)); - this._remoteTerminalChannel.onProcessReplay(e => this._ptys.get(e.id)?.handleReplay(e.event)); + this._remoteTerminalChannel.onProcessReplay(e => { + this._ptys.get(e.id)?.handleReplay(e.event); + if (e.event.commands.commands.length > 0) { + this._onRestoreCommands.fire({ id: e.id, commands: e.event.commands.commands }); + } + }); this._remoteTerminalChannel.onProcessOrphanQuestion(e => this._ptys.get(e.id)?.handleOrphanQuestion()); this._remoteTerminalChannel.onDidRequestDetach(e => this._onDidRequestDetach.fire(e)); this._remoteTerminalChannel.onProcessReady(e => this._ptys.get(e.id)?.handleReady(e.event)); @@ -159,7 +166,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, // TODO: This is ignored - windowsEnableConpty: boolean, // TODO: This is ignored + options: ITerminalProcessOptions, shouldPersist: boolean ): Promise { if (!this._remoteTerminalChannel) { @@ -205,6 +212,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack shellLaunchConfigDto, configuration, activeWorkspaceRootUri, + options, shouldPersist, cols, rows, diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 8f118b3e561..29daf37db0f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -8,14 +8,14 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; -import { INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs, IShellIntegration } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellLaunchConfig, ITerminalDimensions, ITerminalLaunchError, ITerminalProfile, ITerminalTabLayoutInfoById, TerminalIcon, TitleEventSource, TerminalShellType, IExtensionTerminalProfile, TerminalLocation, ProcessPropertyType, IProcessPropertyMap, IShellIntegration } from 'vs/platform/terminal/common/terminal'; +import { INavigationMode, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalFont, ITerminalBackend, ITerminalProcessExtHostProxy, IRegisterContributedProfileArgs } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { IEditableData } from 'vs/workbench/common/views'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; -import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; @@ -473,6 +473,11 @@ export interface ITerminalInstance { */ readonly isDisconnected: boolean; + /* + * Whether this terminal has been disposed of + */ + readonly isDisposed: boolean; + /** * Whether the terminal's pty is hosted on a remote. */ @@ -644,7 +649,7 @@ export interface ITerminalInstance { /** * Copies the terminal selection to the clipboard. */ - copySelection(): Promise; + copySelection(asHtml?: boolean): Promise; /** * Current selection in the terminal. @@ -832,7 +837,7 @@ export interface ITerminalInstance { /** * Triggers a quick pick that displays recent commands or cwds. Selecting one will - * re-run it in the active terminal. + * rerun it in the active terminal. */ runRecent(type: 'command' | 'cwd'): Promise; @@ -891,6 +896,11 @@ export interface IXtermTerminal { * viewport. */ clearBuffer(): void; + + /** + * Clears decorations - for example, when shell integration is disabled. + */ + clearDecorations(): void; } export interface IRequestAddInstanceToGroupEvent { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index a05ec3f927d..d2736a9d75d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -1083,7 +1083,7 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib }, @@ -1105,7 +1105,7 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, when: ContextKeyExpr.and(TerminalContextKeys.focus, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib }, @@ -1127,7 +1127,7 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow, when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, @@ -1149,7 +1149,7 @@ export function registerTerminalActions() { f1: true, category, keybinding: { - mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow, when: TerminalContextKeys.focus, weight: KeybindingWeight.WorkbenchContrib }, @@ -2113,6 +2113,20 @@ export function registerTerminalActions() { await accessor.get(ITerminalService).activeInstance?.copySelection(); } }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TerminalCommandId.CopySelectionAsHtml, + title: { value: localize('workbench.action.terminal.copySelectionAsHtml', "Copy Selection as HTML"), original: 'Copy Selection as HTML' }, + f1: true, + category, + precondition: ContextKeyExpr.and(ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), TerminalContextKeys.textSelected) + }); + } + async run(accessor: ServicesAccessor) { + await accessor.get(ITerminalService).activeInstance?.copySelection(true); + } + }); } if (BrowserFeatures.clipboard.readText) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index a532c23096d..9fa3c648490 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -138,7 +138,13 @@ export class TerminalEditor extends EditorPane { } } else if (event.which === 3) { const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; - if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { + if (rightClickBehavior === 'nothing') { + if (!event.shiftKey) { + this._cancelContextMenu = true; + } + return; + } + else if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { const terminal = this._terminalEditorService.activeInstance; if (!terminal) { return; @@ -175,14 +181,21 @@ export class TerminalEditor extends EditorPane { })); this._register(dom.addDisposableListener(this._editorInstanceElement, 'contextmenu', (event: MouseEvent) => { const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; - if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { - if (!this._cancelContextMenu) { - openContextMenu(event, this._editorInstanceElement!, this._instanceMenu, this._contextMenuService); - } + if (rightClickBehavior === 'nothing' && !event.shiftKey) { event.preventDefault(); event.stopImmediatePropagation(); this._cancelContextMenu = false; + return; } + else + if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') { + if (!this._cancelContextMenu) { + openContextMenu(event, this._editorInstanceElement!, this._instanceMenu, this._contextMenuService); + } + event.preventDefault(); + event.stopImmediatePropagation(); + this._cancelContextMenu = false; + } })); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 12147233d00..f248e093aa8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -14,6 +14,7 @@ import { AutoOpenBarrier, Promises } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { fromNow } from 'vs/base/common/date'; import { debounce } from 'vs/base/common/decorators'; +import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { ISeparator, template } from 'vs/base/common/labels'; @@ -47,7 +48,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { CodeDataTransfers, containsDragType, DragAndDropObserver, IDragAndDropObserverCallbacks } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; @@ -65,8 +66,8 @@ import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/wid import { LineDataEventAddon } from 'vs/workbench/contrib/terminal/browser/xterm/lineDataEventAddon'; import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/xterm/navigationModeAddon'; import { XtermTerminal } from 'vs/workbench/contrib/terminal/browser/xterm/xtermTerminal'; -import { ITerminalCommand, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { TerminalCapabilityStoreMultiplexer } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { getCommandHistory, getDirectoryHistory } from 'vs/workbench/contrib/terminal/common/history'; import { DEFAULT_COMMANDS_TO_SKIP_SHELL, INavigationMode, ITerminalBackend, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, ShellIntegrationExitCode, TerminalCommandId, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -245,6 +246,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } return this._rows; } + get isDisposed(): boolean { return this._isDisposed; } get fixedCols(): number | undefined { return this._fixedCols; } get fixedRows(): number | undefined { return this._fixedRows; } get maxCols(): number { return this._cols; } @@ -493,6 +495,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { e.affectsConfiguration(TerminalSettingId.TerminalDescription)) { this._labelComputer?.refreshLabel(); } + if ((e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) && !this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled)) || + (e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled) && !this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled))) { + this.xterm?.clearDecorations(); + } })); this._workspaceContextService.onDidChangeWorkspaceFolders(() => this._labelComputer?.refreshLabel()); @@ -637,7 +643,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { protected async _createXterm(): Promise { const Terminal = await getXtermConstructor(); if (this._isDisposed) { - throw new Error('Terminal disposed of during xterm.js creation'); + throw new ErrorNoTelemetry('Terminal disposed of during xterm.js creation'); } const xterm = this._instantiationService.createInstance(XtermTerminal, Terminal, this._configHelper, this._cols, this._rows, this.target || TerminalLocation.Panel, this.capabilities); @@ -702,6 +708,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._areLinksReady = true; this._onLinksReady.fire(this); }); + this._processManager.onRestoreCommands(e => this.xterm?.shellIntegration.deserialize(e)); this._loadTypeAheadAddon(xterm); @@ -777,8 +784,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }; if (type === 'command') { - const commands = this.capabilities.get(TerminalCapability.CommandDetection)?.commands; + const cmdDetection = this.capabilities.get(TerminalCapability.CommandDetection); + const commands = cmdDetection?.commands; // Current session history + const executingCommand = cmdDetection?.executingCommand; + if (executingCommand) { + commandMap.add(executingCommand); + } if (commands && commands.length > 0) { for (const entry of commands) { // trim off any whitespace and/or line endings @@ -820,6 +832,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { commandMap.add(label); } items = items.reverse(); + } + if (executingCommand) { + items.unshift({ + label: executingCommand, + description: cmdDetection.cwd + }); + } + if (items.length > 0) { items.unshift({ type: 'separator', label: terminalStrings.currentSessionCategory }); } @@ -829,7 +849,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { for (const [label, info] of history.entries) { // Only add previous session item if it's not in this session if (!commandMap.has(label) && info.shellType === this.shellType) { - previousSessionItems.push({ + previousSessionItems.unshift({ label, buttons: [removeFromCommandHistoryButton] }); @@ -857,7 +877,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Only add previous session item if it's not in this session and it matches the remote authority for (const [label, info] of history.entries) { if ((info === null || info.remoteAuthority === this.remoteAuthority) && !cwds.includes(label)) { - previousSessionItems.push({ + previousSessionItems.unshift({ label, buttons: [removeFromCommandHistoryButton] }); @@ -957,7 +977,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Attach the xterm object to the DOM, exposing it to the smoke tests this._wrapperElement.xterm = xterm.raw; - xterm.attachToElement(xtermElement); + const screenElement = xterm.attachToElement(xtermElement); if (!xterm.raw.element || !xterm.raw.textarea) { throw new Error('xterm elements not set after open'); @@ -1083,7 +1103,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._initDragAndDrop(container); - this._widgetManager.attachToElement(xterm.raw.element); + this._widgetManager.attachToElement(screenElement); this._processManager.onProcessReady((e) => { this._linkManager?.setWidgetManager(this._widgetManager); }); @@ -1113,17 +1133,28 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.focus(); await this.sendPath(path, false); }); - this._dndObserver = new DragAndDropObserver(container, dndController); + this._dndObserver = new dom.DragAndDropObserver(container, dndController); } hasSelection(): boolean { return this.xterm ? this.xterm.raw.hasSelection() : false; } - async copySelection(): Promise { + async copySelection(asHtml?: boolean): Promise { const xterm = await this._xtermReadyPromise; if (this.hasSelection()) { - await this._clipboardService.writeText(xterm.raw.getSelection()); + if (asHtml) { + const selectionAsHtml = await xterm.getSelectionAsHtml(); + function listener(e: any) { + e.clipboardData.setData('text/html', selectionAsHtml); + e.preventDefault(); + } + document.addEventListener('copy', listener); + document.execCommand('copy'); + document.removeEventListener('copy', listener); + } else { + await this._clipboardService.writeText(xterm.raw.getSelection()); + } } else { this._notificationService.warn(nls.localize('terminal.integrated.copySelection.noSelection', 'The terminal has no selection to copy')); } @@ -1717,7 +1748,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { try { const cwd = await this.refreshProperty(ProcessPropertyType.Cwd); if (typeof cwd !== 'string') { - throw new Error('cwd is not a string'); + throw new Error(`cwd is not a string ${cwd}`); } } catch (e: unknown) { // Swallow this as it means the process has been killed @@ -2230,7 +2261,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } -class TerminalInstanceDragAndDropController extends Disposable implements IDragAndDropObserverCallbacks { +class TerminalInstanceDragAndDropController extends Disposable implements dom.IDragAndDropObserverCallbacks { private _dropOverlay?: HTMLElement; private readonly _onDropFile = new Emitter(); @@ -2367,7 +2398,11 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = .monaco-workbench.hc-black .editor-instance .xterm.focus::before, .monaco-workbench.hc-black .pane-body.integrated-terminal .xterm.focus::before, .monaco-workbench.hc-black .editor-instance .xterm:focus::before, - .monaco-workbench.hc-black .pane-body.integrated-terminal .xterm:focus::before { border-color: ${border}; }` + .monaco-workbench.hc-black .pane-body.integrated-terminal .xterm:focus::before, + .monaco-workbench.hc-light .editor-instance .xterm.focus::before, + .monaco-workbench.hc-light .pane-body.integrated-terminal .xterm.focus::before, + .monaco-workbench.hc-light .editor-instance .xterm:focus::before, + .monaco-workbench.hc-light .pane-body.integrated-terminal .xterm:focus::before { border-color: ${border}; }` ); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 94bbabfc054..5183542cb14 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -137,6 +137,17 @@ export function setupTerminalMenus(): void { order: 1 } }, + { + id: MenuId.TerminalInstanceContext, + item: { + command: { + id: TerminalCommandId.CopySelectionAsHtml, + title: localize('workbench.action.terminal.copySelectionAsHtml', "Copy as HTML") + }, + group: ContextMenuGroup.Edit, + order: 2 + } + }, { id: MenuId.TerminalInstanceContext, item: { @@ -145,7 +156,7 @@ export function setupTerminalMenus(): void { title: localize('workbench.action.terminal.paste.short', "Paste") }, group: ContextMenuGroup.Edit, - order: 2 + order: 3 } }, { @@ -237,6 +248,17 @@ export function setupTerminalMenus(): void { order: 1 } }, + { + id: MenuId.TerminalEditorInstanceContext, + item: { + command: { + id: TerminalCommandId.CopySelectionAsHtml, + title: localize('workbench.action.terminal.copySelectionAsHtml', "Copy as HTML") + }, + group: ContextMenuGroup.Edit, + order: 2 + } + }, { id: MenuId.TerminalEditorInstanceContext, item: { @@ -245,7 +267,7 @@ export function setupTerminalMenus(): void { title: localize('workbench.action.terminal.paste.short', "Paste") }, group: ContextMenuGroup.Edit, - order: 2 + order: 3 } }, { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index e9938f084e7..5e749dcac5b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -21,17 +21,18 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IEnvironmentVariableInfo, IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalDimensions, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalLaunchError, FlowControlConstants, ITerminalDimensions, IProcessReadyEvent, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, ITerminalProcessOptions, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { localize } from 'vs/nls'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; -import { NaiveCwdDetectionCapability } from 'vs/workbench/contrib/terminal/common/capabilities/naiveCwdDetectionCapability'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { NaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabilities/naiveCwdDetectionCapability'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { URI } from 'vs/base/common/uri'; +import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -64,7 +65,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce isDisconnected: boolean = false; environmentVariableInfo: IEnvironmentVariableInfo | undefined; backend: ITerminalBackend | undefined; - shellIntegrationAttempted: boolean = false; readonly capabilities = new TerminalCapabilityStore(); private _isDisposed: boolean = false; @@ -106,6 +106,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce readonly onEnvironmentVariableInfoChanged = this._onEnvironmentVariableInfoChange.event; private readonly _onProcessExit = this._register(new Emitter()); readonly onProcessExit = this._onProcessExit.event; + private readonly _onRestoreCommands = this._register(new Emitter()); + readonly onRestoreCommands = this._onRestoreCommands.event; get persistentProcessId(): number | undefined { return this._process?.id; } get shouldPersist(): boolean { return this._process ? this._process.shouldPersist : false; } @@ -245,42 +247,14 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce remoteAuthority: this.remoteAuthority, os: this.os }); + const options: ITerminalProcessOptions = { + shellIntegration: { + enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), + showWelcome: this._configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome), + }, + windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled + }; try { - const shellIntegration = terminalEnvironment.injectShellIntegrationArgs(this._logService, this._configurationService, env, this._configHelper.config.shellIntegration?.enabled || false, shellLaunchConfig, this.os); - this.shellIntegrationAttempted = shellIntegration.enableShellIntegration; - if (this.shellIntegrationAttempted && shellIntegration.args) { - const remoteEnv = await this._remoteAgentService.getEnvironment(); - if (!remoteEnv) { - this._logService.warn('Could not fetch remote environment'); - } else { - if (Array.isArray(shellIntegration.args)) { - // Resolve the arguments manually using the remote server install directory - const appRoot = remoteEnv.appRoot; - let appRootOsPath = remoteEnv.appRoot.fsPath; - if (OS === OperatingSystem.Windows && remoteEnv.os !== OperatingSystem.Windows) { - // Local Windows, remote POSIX - appRootOsPath = appRoot.path.replace(/\\/g, '/'); - } else if (OS !== OperatingSystem.Windows && remoteEnv.os === OperatingSystem.Windows) { - // Local POSIX, remote Windows - appRootOsPath = appRoot.path.replace(/\//g, '\\'); - } - for (let i = 0; i < shellIntegration.args.length; i++) { - shellIntegration.args[i] = shellIntegration.args[i].replace('${execInstallFolder}', appRootOsPath); - } - } - shellLaunchConfig.args = shellIntegration.args; - } - } - //TODO: fix - if (env?.['VSCODE_SHELL_LOGIN']) { - shellLaunchConfig.env = shellLaunchConfig.env || {} as IProcessEnvironment; - shellLaunchConfig.env['VSCODE_SHELL_LOGIN'] = '1'; - } - if (env?.['_ZDOTDIR']) { - shellLaunchConfig.env = shellLaunchConfig.env || {} as IProcessEnvironment; - shellLaunchConfig.env['_ZDOTDIR'] = '1'; - } - newProcess = await backend.createProcess( shellLaunchConfig, '', // TODO: Fix cwd @@ -288,7 +262,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce rows, this._configHelper.config.unicodeVersion, env, // TODO: - true, // TODO: Fix enable + options, shouldPersist ); } catch (e) { @@ -363,6 +337,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._onDidChangeProperty.fire({ type, value }); }) ]; + if (newProcess.onRestoreCommands) { + this._processListeners.push(newProcess.onRestoreCommands(e => { + this._onRestoreCommands.fire(e); + })); + } setTimeout(() => { if (this.processState === ProcessState.Launching) { @@ -455,23 +434,15 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce const env = await this._resolveEnvironment(backend, variableResolver, shellLaunchConfig); - const shellIntegration = terminalEnvironment.injectShellIntegrationArgs(this._logService, this._configurationService, env, this._configHelper.config.shellIntegration?.enabled || false, shellLaunchConfig, OS); - if (shellIntegration.enableShellIntegration) { - shellLaunchConfig.args = shellIntegration.args; - if (env?.['_ZDOTDIR']) { - shellLaunchConfig.env = shellLaunchConfig.env || {} as IProcessEnvironment; - shellLaunchConfig.env['_ZDOTDIR'] = '1'; - } - // Always resolve the injected arguments on local processes - await this._terminalProfileResolverService.resolveShellLaunchConfig(shellLaunchConfig, { - remoteAuthority: undefined, - os: OS - }); - } - this.shellIntegrationAttempted = shellIntegration.enableShellIntegration; - const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled; + const options: ITerminalProcessOptions = { + shellIntegration: { + enabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled), + showWelcome: this._configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome), + }, + windowsEnableConpty: this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled + }; const shouldPersist = this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isFeatureTerminal; - return await backend.createProcess(shellLaunchConfig, initialCwd, cols, rows, this._configHelper.config.unicodeVersion, env, useConpty, shouldPersist); + return await backend.createProcess(shellLaunchConfig, initialCwd, cols, rows, this._configHelper.config.unicodeVersion, env, options, shouldPersist); } private _setupPtyHostListeners(backend: ITerminalBackend) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index ea7df0695af..31995a09e77 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -639,7 +639,7 @@ export class TerminalService implements ITerminalService { @debounce(500) private _updateTitle(instance?: ITerminalInstance): void { - if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.title) { + if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.title || instance.isDisposed) { return; } if (instance.staticTitle) { @@ -651,7 +651,7 @@ export class TerminalService implements ITerminalService { @debounce(500) private _updateIcon(instance?: ITerminalInstance): void { - if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.icon) { + if (!this.configHelper.config.enablePersistentSessions || !instance || !instance.persistentProcessId || !instance.icon || instance.isDisposed) { return; } this._primaryBackend?.updateIcon(instance.persistentProcessId, instance.icon, instance.color); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index c0ba0a11000..c20833c2e64 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -367,7 +367,13 @@ export class TerminalTabbedView extends Disposable { terminal.focus(); } else if (event.which === 3) { const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; - if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { + if (rightClickBehavior === 'nothing') { + if (!event.shiftKey) { + this._cancelContextMenu = true; + } + return; + } + else if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') { // copyPaste: Shift+right click should open context menu if (rightClickBehavior === 'copyPaste' && event.shiftKey) { openContextMenu(event, this._parentElement, this._instanceMenu, this._contextMenuService); @@ -398,6 +404,10 @@ export class TerminalTabbedView extends Disposable { } })); this._register(dom.addDisposableListener(terminalContainer, 'contextmenu', (event: MouseEvent) => { + const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; + if (rightClickBehavior === 'nothing' && !event.shiftKey) { + this._cancelContextMenu = true; + } if (!this._cancelContextMenu) { openContextMenu(event, this._parentElement, this._instanceMenu, this._contextMenuService); } @@ -406,6 +416,10 @@ export class TerminalTabbedView extends Disposable { this._cancelContextMenu = false; })); this._register(dom.addDisposableListener(this._tabContainer, 'contextmenu', (event: MouseEvent) => { + const rightClickBehavior = this._terminalService.configHelper.config.rightClickBehavior; + if (rightClickBehavior === 'nothing' && !event.shiftKey) { + this._cancelContextMenu = true; + } if (!this._cancelContextMenu) { const emptyList = this._tabList.getFocus().length === 0; openContextMenu(event, this._parentElement, emptyList ? this._tabsListEmptyMenu : this._tabsListMenu, this._contextMenuService, emptyList ? this._getTabActions() : undefined); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index e57c8613a99..ee021996cb1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; function getCapabilityName(capability: TerminalCapability): string | undefined { switch (capability) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index dc49e71f0c7..03f433bd72b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -110,6 +110,18 @@ export class TerminalViewPane extends ViewPane { this._terminalTabbedView?.rerenderTabs(); } })); + configurationService.onDidChangeConfiguration(e => { + if ((e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) && !configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled)) || + (e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled) && !configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled))) { + this._parentDomElement?.classList.remove('shell-integration'); + } else if (configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled) && configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled)) { + this._parentDomElement?.classList.add('shell-integration'); + } + }); + + if (configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled) && configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled)) { + this._parentDomElement?.classList.add('shell-integration'); + } } override renderBody(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index cf8f8c46cdd..c015758c701 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -13,6 +13,7 @@ export interface IXtermCore { viewport?: { _innerRefresh(): void; }; + _onData: IEventEmitter; _onKey: IEventEmitter<{ key: string }>; _charSizeService: { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts similarity index 72% rename from src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts rename to src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts index f99ade0c8cc..c7b911c633d 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon.ts @@ -6,8 +6,12 @@ import { coalesce } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICommandTracker } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { ICommandDetectionCapability, IPartialCommandDetectionCapability, ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import type { Terminal, IMarker, ITerminalAddon } from 'xterm'; +import { ICommandDetectionCapability, IPartialCommandDetectionCapability, ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import type { Terminal, IMarker, ITerminalAddon, IDecoration } from 'xterm'; +import { timeout } from 'vs/base/common/async'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; enum Boundary { Top, @@ -19,11 +23,12 @@ export const enum ScrollPosition { Middle } -export class CommandTrackerAddon extends Disposable implements ICommandTracker, ITerminalAddon { +export class CommandNavigationAddon extends Disposable implements ICommandTracker, ITerminalAddon { private _currentMarker: IMarker | Boundary = Boundary.Bottom; private _selectionStart: IMarker | Boundary | null = null; private _isDisposable: boolean = false; protected _terminal: Terminal | undefined; + private _navigationDecoration: IDecoration | undefined; private _commandDetection?: ICommandDetectionCapability | IPartialCommandDetectionCapability; @@ -31,7 +36,10 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, this._terminal = terminal; } - constructor(store: ITerminalCapabilityStore) { + constructor( + store: ITerminalCapabilityStore, + @IThemeService private readonly _themeService: IThemeService + ) { super(); this._refreshActiveCapability(store); this._register(store.onDidAddCapability(() => this._refreshActiveCapability(store))); @@ -65,7 +73,7 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, this._selectionStart = null; } - scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + scrollToPreviousCommand(scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false): void { if (!this._terminal) { return; } @@ -74,9 +82,11 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, } let markerIndex; - const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY); + const currentLineY = typeof this._currentMarker === 'object' + ? this._getTargetScrollLine(this._terminal, this._currentMarker, scrollPosition) + : Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY); const viewportY = this._terminal.buffer.active.viewportY; - if (!retainSelection && currentLineY !== viewportY) { + if (typeof this._currentMarker === 'object' ? !this._isMarkerInViewport(this._terminal, this._currentMarker) : currentLineY !== viewportY) { // The user has scrolled, find the line based on the current scroll position. This only // works when not retaining selection const markersBelowViewport = this._getCommandMarkers().filter(e => e.line >= viewportY).length; @@ -104,7 +114,7 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, this._scrollToMarker(this._currentMarker, scrollPosition); } - scrollToNextCommand(scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + scrollToNextCommand(scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false): void { if (!this._terminal) { return; } @@ -113,9 +123,11 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, } let markerIndex; - const currentLineY = Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY); + const currentLineY = typeof this._currentMarker === 'object' + ? this._getTargetScrollLine(this._terminal, this._currentMarker, scrollPosition) + : Math.min(this._getLine(this._terminal, this._currentMarker), this._terminal.buffer.active.baseY); const viewportY = this._terminal.buffer.active.viewportY; - if (!retainSelection && currentLineY !== viewportY) { + if (typeof this._currentMarker === 'object' ? !this._isMarkerInViewport(this._terminal, this._currentMarker) : currentLineY !== viewportY) { // The user has scrolled, find the line based on the current scroll position. This only // works when not retaining selection const markersAboveViewport = this._getCommandMarkers().filter(e => e.line <= viewportY).length; @@ -147,11 +159,55 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, if (!this._terminal) { return; } - let line = marker.line; - if (position === ScrollPosition.Middle) { - line = Math.max(line - Math.floor(this._terminal.rows / 2), 0); + if (!this._isMarkerInViewport(this._terminal, marker)) { + const line = this._getTargetScrollLine(this._terminal, marker, position); + this._terminal.scrollToLine(line); } - this._terminal.scrollToLine(line); + this._navigationDecoration?.dispose(); + const color = this._themeService.getColorTheme().getColor(TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR); + + const decoration = this._terminal.registerDecoration({ + marker, + width: this._terminal.cols, + overviewRulerOptions: { + color: color?.toString() || '#a0a0a0cc' + } + }); + this._navigationDecoration = decoration; + if (decoration) { + let isRendered = false; + decoration.onRender(element => { + if (!isRendered) { + // TODO: Remove when https://github.com/xtermjs/xterm.js/issues/3686 is fixed + if (!element.classList.contains('xterm-decoration-overview-ruler')) { + element.classList.add('terminal-scroll-highlight'); + } + } + }); + decoration.onDispose(() => { + if (decoration === this._navigationDecoration) { + this._navigationDecoration = undefined; + } + }); + // Number picked to align with symbol highlight in the editor + timeout(350).then(() => { + decoration.dispose(); + }); + } + } + + private _getTargetScrollLine(terminal: Terminal, marker: IMarker, position: ScrollPosition) { + // Middle is treated at 1/4 of the viewport's size because context below is almost always + // more important than context above in the terminal. + if (position === ScrollPosition.Middle) { + return Math.max(marker.line - Math.floor(terminal.rows / 4), 0); + } + return marker.line; + } + + private _isMarkerInViewport(terminal: Terminal, marker: IMarker) { + const viewportY = terminal.buffer.active.viewportY; + return marker.line >= viewportY && marker.line < viewportY + terminal.rows; } selectToPreviousCommand(): void { @@ -232,7 +288,7 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, return marker.line; } - scrollToPreviousLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + scrollToPreviousLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false): void { if (!retainSelection) { this._selectionStart = null; } @@ -255,7 +311,7 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, this._scrollToMarker(this._currentMarker, scrollPosition); } - scrollToNextLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Top, retainSelection: boolean = false): void { + scrollToNextLine(xterm: Terminal, scrollPosition: ScrollPosition = ScrollPosition.Middle, retainSelection: boolean = false): void { if (!retainSelection) { this._selectionStart = null; } @@ -332,3 +388,11 @@ export class CommandTrackerAddon extends Disposable implements ICommandTracker, return this._getCommandMarkers().length; } } + +registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const focusBorderColor = theme.getColor(focusBorder); + + if (focusBorderColor) { + collector.addRule(`.terminal-scroll-highlight { border-color: ${focusBorderColor.toString()}; } `); + } +}); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index bafeec8a433..bec11a9032d 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -8,8 +8,8 @@ import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal' import { IDecoration, ITerminalAddon, Terminal } from 'xterm'; import * as dom from 'vs/base/browser/dom'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { IColorTheme, ICssStyleCollector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { IAction } from 'vs/base/common/actions'; @@ -22,6 +22,7 @@ import { fromNow } from 'vs/base/common/date'; import { toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { Color } from 'vs/base/common/color'; const enum DecorationSelector { CommandDecoration = 'terminal-command-decoration', @@ -29,13 +30,12 @@ const enum DecorationSelector { DefaultColor = 'default', Codicon = 'codicon', XtermDecoration = 'xterm-decoration', - FirstSplitContainer = '.pane-body.integrated-terminal .terminal-group .monaco-split-view2.horizontal .split-view-view:first-child .xterm' + OverviewRuler = 'xterm-decoration-overview-ruler' } const enum DecorationStyles { DefaultDimension = 16, - MarginLeftFirstSplit = -17, - MarginLeft = -12 + MarginLeft = -17, } interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number } @@ -57,7 +57,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { @IClipboardService private readonly _clipboardService: IClipboardService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IHoverService private readonly _hoverService: IHoverService, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IThemeService private readonly _themeService: IThemeService ) { super(); this._attachToCommandCapability(); @@ -69,15 +70,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIcon) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIconSuccess) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationIconError)) { - this._refreshClasses(); + this._refreshStyles(); } else if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { this.refreshLayouts(); - } else if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) && !this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled)) { - this._commandStartedListener?.dispose(); - this._commandFinishedListener?.dispose(); - this._clearDecorations(); } }); + this._themeService.onDidColorThemeChange(() => this._refreshStyles(true)); } public refreshLayouts(): void { @@ -87,14 +85,33 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } } - private _refreshClasses(): void { + private _refreshStyles(refreshOverviewRulerColors?: boolean): void { + if (refreshOverviewRulerColors) { + for (const decoration of this._decorations.values()) { + let color = decoration.exitCode === undefined ? defaultColor : decoration.exitCode ? errorColor : successColor; + if (color && typeof color !== 'string') { + color = color.toString(); + } else { + color = ''; + } + if (decoration.decoration.overviewRulerOptions) { + decoration.decoration.overviewRulerOptions.color = color; + } else { + decoration.decoration.overviewRulerOptions = { color }; + } + } + } this._updateClasses(this._placeholderDecoration?.element); for (const decoration of this._decorations.values()) { this._updateClasses(decoration.decoration.element, decoration.exitCode); } } - private _clearDecorations(): void { + public clearDecorations(disableDecorations?: boolean): void { + if (disableDecorations) { + this._commandStartedListener?.dispose(); + this._commandFinishedListener?.dispose(); + } this._placeholderDecoration?.dispose(); this._placeholderDecoration?.marker.dispose(); for (const value of this._decorations.values()) { @@ -133,6 +150,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (!capability) { return; } + if (capability.commands.length > 0) { + const lastCommand = capability.commands[capability.commands.length - 1]; + if (lastCommand.marker && !lastCommand.endMarker) { + this.registerCommandDecoration(lastCommand, true); + } + } this._commandStartedListener = capability.onCommandStarted(command => this.registerCommandDecoration(command, true)); } @@ -145,16 +168,21 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { if (!capability) { return; } + for (const command of capability.commands) { + this.registerCommandDecoration(command); + } this._commandFinishedListener = capability.onCommandFinished(command => { - if (this._placeholderDecoration?.marker.id) { - this._decorations.delete(this._placeholderDecoration?.marker.id); + if (command.command.trim().toLowerCase() === 'clear' || command.command.trim().toLowerCase() === 'cls') { + this.clearDecorations(); + return; } - this._placeholderDecoration?.dispose(); this.registerCommandDecoration(command); }); } - activate(terminal: Terminal): void { this._terminal = terminal; } + activate(terminal: Terminal): void { + this._terminal = terminal; + } registerCommandDecoration(command: ITerminalCommand, beforeCommandExecution?: boolean): IDecoration | undefined { if (!this._terminal) { @@ -164,14 +192,30 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { throw new Error(`cannot add a decoration for a command ${JSON.stringify(command)} with no marker`); } - const decoration = this._terminal.registerDecoration({ marker: command.marker }); + this._placeholderDecoration?.dispose(); + let color = command.exitCode === undefined ? defaultColor : command.exitCode ? errorColor : successColor; + if (color && typeof color !== 'string') { + color = color.toString(); + } else { + color = ''; + } + const decoration = this._terminal.registerDecoration({ + marker: command.marker, + overviewRulerOptions: beforeCommandExecution ? undefined : { color, position: command.exitCode ? 'right' : 'left' } + }); if (!decoration) { return undefined; } + decoration.onRender(element => { - if (beforeCommandExecution) { + if (element.classList.contains(DecorationSelector.OverviewRuler)) { + return; + } + if (beforeCommandExecution && !this._placeholderDecoration) { this._placeholderDecoration = decoration; + this._placeholderDecoration.onDispose(() => this._placeholderDecoration = undefined); } else { + decoration.onDispose(() => this._decorations.delete(decoration.marker.id)); this._decorations.set(decoration.marker.id, { decoration, @@ -179,9 +223,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { exitCode: command.exitCode }); } - - if (!element.classList.contains(DecorationSelector.Codicon)) { - // first render + if (!element.classList.contains(DecorationSelector.Codicon) || command.marker?.line === 0) { + // first render or buffer was cleared this._updateLayout(element); this._updateClasses(element, command.exitCode); } @@ -198,18 +241,11 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { const lineHeight = this._configurationService.inspect(TerminalSettingId.LineHeight).value; if (typeof fontSize === 'number' && typeof defaultFontSize === 'number' && typeof lineHeight === 'number') { const scalar = (fontSize / defaultFontSize) <= 1 ? (fontSize / defaultFontSize) : 1; - // must be inlined to override the inlined styles from xterm element.style.width = `${scalar * DecorationStyles.DefaultDimension}px`; element.style.height = `${scalar * DecorationStyles.DefaultDimension * lineHeight}px`; element.style.fontSize = `${scalar * DecorationStyles.DefaultDimension}px`; - - // the first split terminal in the panel has more room - if (element.closest(DecorationSelector.FirstSplitContainer)) { - element.style.marginLeft = `${scalar * DecorationStyles.MarginLeftFirstSplit}px`; - } else { - element.style.marginLeft = `${scalar * DecorationStyles.MarginLeft}px`; - } + element.style.marginLeft = `${scalar * DecorationStyles.MarginLeft}px`; } } @@ -282,17 +318,19 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { }); } actions.push({ - class: 'rerun-command', tooltip: 'Rerun Command', dispose: () => { }, id: 'terminal.rerunCommand', label: localize("terminal.rerunCommand", 'Re-run Command'), enabled: true, + class: 'rerun-command', tooltip: 'Rerun Command', dispose: () => { }, id: 'terminal.rerunCommand', label: localize("terminal.rerunCommand", 'Rerun Command'), enabled: true, run: () => this._onDidRequestRunCommand.fire(command.command) }); return actions; } } - +let successColor: string | Color | undefined; +let errorColor: string | Color | undefined; +let defaultColor: string | Color | undefined; registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const successColor = theme.getColor(TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR); - const errorColor = theme.getColor(TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR); - const defaultColor = theme.getColor(TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR); + successColor = theme.getColor(TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR); + errorColor = theme.getColor(TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR); + defaultColor = theme.getColor(TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR); const hoverBackgroundColor = theme.getColor(toolbarHoverBackground); if (successColor) { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 5d35069c2fa..ce8217f159e 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -7,20 +7,21 @@ import type { IBuffer, ITheme, RendererType, Terminal as RawXtermTerminal } from import type { ISearchOptions, SearchAddon as SearchAddonType } from 'xterm-addon-search'; import type { Unicode11Addon as Unicode11AddonType } from 'xterm-addon-unicode11'; import type { WebglAddon as WebglAddonType } from 'xterm-addon-webgl'; +import { SerializeAddon as SerializeAddonType } from 'xterm-addon-serialize'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; -import { IShellIntegration, ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IShellIntegration, TerminalLocation, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { ITerminalFont, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { isSafari } from 'vs/base/browser/browser'; import { ICommandTracker, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; +import { CommandNavigationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandNavigationAddon'; import { localize } from 'vs/nls'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; @@ -28,10 +29,10 @@ import { editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TERMINAL_FOREGROUND_COLOR, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, ansiColorIdentifiers, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { Color } from 'vs/base/common/color'; -import { ShellIntegrationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon'; +import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon'; -import { ITerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Emitter } from 'vs/base/common/event'; // How long in milliseconds should an average frame take to render for a notification to appear @@ -42,6 +43,7 @@ const NUMBER_OF_FRAMES_TO_MEASURE = 20; let SearchAddon: typeof SearchAddonType; let Unicode11Addon: typeof Unicode11AddonType; let WebglAddon: typeof WebglAddonType; +let SerializeAddon: typeof SerializeAddonType; /** * Wraps the xterm object with additional functionality. Interaction with the backing process is out @@ -56,7 +58,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { private _container?: HTMLElement; // Always on addons - private _commandTrackerAddon: CommandTrackerAddon; + private _commandNavigationAddon: CommandNavigationAddon; private _shellIntegrationAddon: ShellIntegrationAddon; private _decorationAddon: DecorationAddon | undefined; @@ -64,11 +66,12 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { private _searchAddon?: SearchAddonType; private _unicode11Addon?: Unicode11AddonType; private _webglAddon?: WebglAddonType; + private _serializeAddon?: SerializeAddonType; private readonly _onDidRequestRunCommand = new Emitter(); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; - get commandTracker(): ICommandTracker { return this._commandTrackerAddon; } + get commandTracker(): ICommandTracker { return this._commandNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } private _target: TerminalLocation | undefined; @@ -127,7 +130,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { fastScrollSensitivity: config.fastScrollSensitivity, scrollSensitivity: config.mouseWheelScrollSensitivity, rendererType: this._getBuiltInXtermRenderer(config.gpuAcceleration, XtermTerminal._suggestedRendererType), - wordSeparator: config.wordSeparators + wordSeparator: config.wordSeparators, + overviewRulerWidth: 10 })); this._core = (this.raw as any)._core as IXtermCore; @@ -153,8 +157,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // Load addons this._updateUnicodeVersion(); - this._commandTrackerAddon = new CommandTrackerAddon(capabilities); - this.raw.loadAddon(this._commandTrackerAddon); + this._commandNavigationAddon = this._instantiationService.createInstance(CommandNavigationAddon, capabilities); + this.raw.loadAddon(this._commandNavigationAddon); this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon); this.raw.loadAddon(this._shellIntegrationAddon); if (this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) && this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled)) { @@ -167,7 +171,16 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { this.raw.loadAddon(this._decorationAddon); } - attachToElement(container: HTMLElement) { + async getSelectionAsHtml(): Promise { + if (!this._serializeAddon) { + const Addon = await this._getSerializeAddonConstructor(); + this._serializeAddon = new Addon(); + this.raw.loadAddon(this._serializeAddon); + } + return this._serializeAddon.serializeAsHTML({ onlySelection: true }); + } + + attachToElement(container: HTMLElement): HTMLElement { // Update the theme when attaching as the terminal location could have changed this._updateTheme(); if (!this._container) { @@ -177,6 +190,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { if (this._shouldLoadWebgl()) { this._enableWebglRenderer(); } + // Screen must be created at this point as xterm.open is called + return this._container.querySelector('.xterm-screen')!; } updateConfig(): void { @@ -214,6 +229,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { this.raw.clearTextureAtlas(); } + clearDecorations(): void { + this._decorationAddon?.clearDecorations(true); + } + forceRefresh() { this._core.viewport?._innerRefresh(); @@ -312,6 +331,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { clearBuffer(): void { this.raw.clear(); + // hack so that the next placeholder shows + this._decorationAddon?.registerCommandDecoration({ marker: this.raw.registerMarker(0), hasOutput: false, timestamp: Date.now(), getOutput: () => { return undefined; }, command: '' }, true); } private _setCursorBlink(blink: boolean): void { @@ -396,6 +417,13 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { return WebglAddon; } + protected async _getSerializeAddonConstructor(): Promise { + if (!SerializeAddon) { + SerializeAddon = (await import('xterm-addon-serialize')).SerializeAddon; + } + return SerializeAddon; + } + private _disposeOfWebglRenderer(): void { try { this._webglAddon?.dispose(); diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index c0521b853b6..e8c7da68932 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -18,7 +18,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { Schemas } from 'vs/base/common/network'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfigDto, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, TitleEventSource, ISerializedTerminalState, IPtyHostController } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IRequestResolveVariablesEvent, IShellLaunchConfigDto, ITerminalLaunchError, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, IProcessProperty, ProcessPropertyType, IProcessPropertyMap, TitleEventSource, ISerializedTerminalState, IPtyHostController, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { ICompleteTerminalConfiguration } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -44,6 +44,7 @@ export interface ICreateTerminalProcessArguments { activeWorkspaceFolder: IWorkspaceFolderData | null; activeFileResource: UriComponents | undefined; shouldPersistTerminal: boolean; + options: ITerminalProcessOptions; cols: number; rows: number; unicodeVersion: '6' | '11'; @@ -114,7 +115,16 @@ export class RemoteTerminalChannelClient implements IPtyHostController { return this._channel.call('$restartPtyHost', []); } - async createProcess(shellLaunchConfig: IShellLaunchConfigDto, configuration: ICompleteTerminalConfiguration, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, unicodeVersion: '6' | '11'): Promise { + async createProcess( + shellLaunchConfig: IShellLaunchConfigDto, + configuration: ICompleteTerminalConfiguration, + activeWorkspaceRootUri: URI | undefined, + options: ITerminalProcessOptions, + shouldPersistTerminal: boolean, + cols: number, + rows: number, + unicodeVersion: '6' | '11' + ): Promise { // Be sure to first wait for the remote configuration await this._configurationService.whenRemoteConfigurationLoaded(); @@ -153,7 +163,7 @@ export class RemoteTerminalChannelClient implements IPtyHostController { const activeFileResource = EditorResourceAccessor.getOriginalUri(this._editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.userData, Schemas.vscodeRemote] + filterByScheme: [Schemas.file, Schemas.vscodeUserData, Schemas.vscodeRemote] }); const args: ICreateTerminalProcessArguments = { @@ -167,6 +177,7 @@ export class RemoteTerminalChannelClient implements IPtyHostController { activeWorkspaceFolder, activeFileResource, shouldPersistTerminal, + options, cols, rows, unicodeVersion, diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index a12cccbc694..46757b2703e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,13 +8,13 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; +import { IProcessDetails, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ITerminalCapabilityStore, IXtermMarker } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityStore, IXtermMarker } from 'vs/platform/terminal/common/capabilities/capabilities'; export const TERMINAL_VIEW_ID = 'terminal'; @@ -138,7 +138,7 @@ export interface ITerminalBackend { rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, - windowsEnableConpty: boolean, + options: ITerminalProcessOptions, shouldPersist: boolean ): Promise; } @@ -231,7 +231,7 @@ export interface ITerminalConfiguration { macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; gpuAcceleration: 'auto' | 'on' | 'canvas' | 'off'; - rightClickBehavior: 'default' | 'copyPaste' | 'paste' | 'selectWord'; + rightClickBehavior: 'default' | 'copyPaste' | 'paste' | 'selectWord' | 'nothing'; cursorBlinking: boolean; cursorStyle: 'block' | 'underline' | 'line'; cursorWidth: number; @@ -328,10 +328,6 @@ export interface IRemoteTerminalAttachTarget { fixedDimensions: IFixedTerminalDimensions | undefined; } -export interface IShellIntegration { - capabilities: ITerminalCapabilityStore; -} - export interface ITerminalCommand { command: string; timestamp: number; @@ -386,6 +382,7 @@ export interface ITerminalProcessManager extends IDisposable { readonly onEnvironmentVariableInfoChanged: Event; readonly onDidChangeProperty: Event>; readonly onProcessExit: Event; + readonly onRestoreCommands: Event; dispose(immediate?: boolean): void; detachFromProcess(): Promise; @@ -475,6 +472,7 @@ export const enum TerminalCommandId { RunRecentCommand = 'workbench.action.terminal.runRecentCommand', GoToRecentDirectory = 'workbench.action.terminal.goToRecentDirectory', CopySelection = 'workbench.action.terminal.copySelection', + CopySelectionAsHtml = 'workbench.action.terminal.copySelectionAsHtml', SelectAll = 'workbench.action.terminal.selectAll', DeleteWordLeft = 'workbench.action.terminal.deleteWordLeft', DeleteWordRight = 'workbench.action.terminal.deleteWordRight', @@ -566,6 +564,7 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ TerminalCommandId.ClearSelection, TerminalCommandId.Clear, TerminalCommandId.CopySelection, + TerminalCommandId.CopySelectionAsHtml, TerminalCommandId.DeleteToLineStart, TerminalCommandId.DeleteWordLeft, TerminalCommandId.DeleteWordRight, diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index 8ac032a5a36..4b8cdc2609d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Color, RGBA } from 'vs/base/common/color'; import * as nls from 'vs/nls'; import { registerColor, ColorIdentifier, ColorDefaults } from 'vs/platform/theme/common/colorRegistry'; @@ -19,44 +18,58 @@ export const TERMINAL_BACKGROUND_COLOR = registerColor('terminal.background', nu export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', { light: '#333333', dark: '#CCCCCC', - hc: '#FFFFFF' + hcDark: '#FFFFFF', + hcLight: '#292929' }, nls.localize('terminal.foreground', 'The foreground color of the terminal.')); export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.')); export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.')); export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', { light: '#00000040', dark: '#FFFFFF40', - hc: '#FFFFFF80' + hcDark: '#FFFFFF80', + hcLight: '#F2F2F2' }, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', { light: '#00000040', dark: '#ffffff40', - hc: '#ffffff80' + hcDark: '#ffffff80', + hcLight: '#00000040', }, nls.localize('terminalCommandDecoration.defaultBackground', 'The default terminal command decoration background color.')); export const TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.successBackground', { - dark: new Color(new RGBA(12, 125, 157)), - light: new Color(new RGBA(102, 175, 224)), - hc: new Color(new RGBA(0, 155, 249)) + dark: '#1B81A8', + light: '#2090D3', + hcDark: '#1B81A8', + hcLight: '#007100' }, nls.localize('terminalCommandDecoration.successBackground', 'The terminal command decoration background color for successful commands.')); export const TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.errorBackground', { - dark: new Color(new RGBA(148, 21, 27)), - light: new Color(new RGBA(202, 75, 81)), - hc: new Color(new RGBA(252, 93, 109)) + dark: '#F14C4C', + light: '#E51400', + hcDark: '#F14C4C', + hcLight: '#B5200D' }, nls.localize('terminalCommandDecoration.errorBackground', 'The terminal command decoration background color for error commands.')); +export const TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR = registerColor('terminalOverviewRuler.cursorForeground', { + dark: '#A0A0A0CC', + light: '#A0A0A0CC', + hcDark: '#A0A0A0CC', + hcLight: '#A0A0A0CC' +}, nls.localize('terminalOverviewRuler.cursorForeground', 'The overview ruler cursor color.')); export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', { dark: PANEL_BORDER, light: PANEL_BORDER, - hc: PANEL_BORDER + hcDark: PANEL_BORDER, + hcLight: PANEL_BORDER }, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); export const TERMINAL_DRAG_AND_DROP_BACKGROUND = registerColor('terminal.dropBackground', { dark: EDITOR_DRAG_AND_DROP_BACKGROUND, light: EDITOR_DRAG_AND_DROP_BACKGROUND, - hc: EDITOR_DRAG_AND_DROP_BACKGROUND + hcDark: EDITOR_DRAG_AND_DROP_BACKGROUND, + hcLight: EDITOR_DRAG_AND_DROP_BACKGROUND }, nls.localize('terminal.dragAndDropBackground', "Background color when dragging on top of terminals. The color should have transparency so that the terminal contents can still shine through.")); export const TERMINAL_TAB_ACTIVE_BORDER = registerColor('terminal.tab.activeBorder', { dark: TAB_ACTIVE_BORDER, light: TAB_ACTIVE_BORDER, - hc: TAB_ACTIVE_BORDER + hcDark: TAB_ACTIVE_BORDER, + hcLight: TAB_ACTIVE_BORDER }, nls.localize('terminal.tab.activeBorder', 'Border on the side of the terminal tab in the panel. This defaults to tab.activeBorder.')); export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefaults } } = { @@ -65,7 +78,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#000000', dark: '#000000', - hc: '#000000' + hcDark: '#000000', + hcLight: '#292929' } }, 'terminal.ansiRed': { @@ -73,7 +87,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#cd3131', dark: '#cd3131', - hc: '#cd0000' + hcDark: '#cd0000', + hcLight: '#cd3131' } }, 'terminal.ansiGreen': { @@ -81,7 +96,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#00BC00', dark: '#0DBC79', - hc: '#00cd00' + hcDark: '#00cd00', + hcLight: '#00bc00' } }, 'terminal.ansiYellow': { @@ -89,7 +105,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#949800', dark: '#e5e510', - hc: '#cdcd00' + hcDark: '#cdcd00', + hcLight: '#949800' } }, 'terminal.ansiBlue': { @@ -97,7 +114,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#0451a5', dark: '#2472c8', - hc: '#0000ee' + hcDark: '#0000ee', + hcLight: '#0451a5' } }, 'terminal.ansiMagenta': { @@ -105,7 +123,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#bc05bc', dark: '#bc3fbc', - hc: '#cd00cd' + hcDark: '#cd00cd', + hcLight: '#bc05bc' } }, 'terminal.ansiCyan': { @@ -113,7 +132,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#0598bc', dark: '#11a8cd', - hc: '#00cdcd' + hcDark: '#00cdcd', + hcLight: '#0598b' } }, 'terminal.ansiWhite': { @@ -121,7 +141,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#555555', dark: '#e5e5e5', - hc: '#e5e5e5' + hcDark: '#e5e5e5', + hcLight: '#555555' } }, 'terminal.ansiBrightBlack': { @@ -129,7 +150,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#666666', dark: '#666666', - hc: '#7f7f7f' + hcDark: '#7f7f7f', + hcLight: '#666666' } }, 'terminal.ansiBrightRed': { @@ -137,7 +159,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#cd3131', dark: '#f14c4c', - hc: '#ff0000' + hcDark: '#ff0000', + hcLight: '#cd3131' } }, 'terminal.ansiBrightGreen': { @@ -145,7 +168,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#14CE14', dark: '#23d18b', - hc: '#00ff00' + hcDark: '#00ff00', + hcLight: '#00bc00' } }, 'terminal.ansiBrightYellow': { @@ -153,7 +177,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#b5ba00', dark: '#f5f543', - hc: '#ffff00' + hcDark: '#ffff00', + hcLight: '#b5ba00' } }, 'terminal.ansiBrightBlue': { @@ -161,7 +186,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#0451a5', dark: '#3b8eea', - hc: '#5c5cff' + hcDark: '#5c5cff', + hcLight: '#0451a5' } }, 'terminal.ansiBrightMagenta': { @@ -169,7 +195,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#bc05bc', dark: '#d670d6', - hc: '#ff00ff' + hcDark: '#ff00ff', + hcLight: '#bc05bc' } }, 'terminal.ansiBrightCyan': { @@ -177,7 +204,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#0598bc', dark: '#29b8db', - hc: '#00ffff' + hcDark: '#00ffff', + hcLight: '#0598bc' } }, 'terminal.ansiBrightWhite': { @@ -185,7 +213,8 @@ export const ansiColorMap: { [key: string]: { index: number; defaults: ColorDefa defaults: { light: '#a5a5a5', dark: '#e5e5e5', - hc: '#ffffff' + hcDark: '#ffffff', + hcLight: '#a5a5a5' } } }; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 3b2bebc9852..61ac9201841 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -186,9 +186,9 @@ const terminalConfiguration: IConfigurationNode = { default: DEFAULT_LINE_HEIGHT }, [TerminalSettingId.MinimumContrastRatio]: { - markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: The default, do nothing.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), type: 'number', - default: 1 + default: 4.5 }, [TerminalSettingId.FastScrollSensitivity]: { markdownDescription: localize('terminal.integrated.fastScrollSensitivity', "Scrolling speed multiplier when pressing `Alt`."), @@ -303,12 +303,13 @@ const terminalConfiguration: IConfigurationNode = { }, [TerminalSettingId.RightClickBehavior]: { type: 'string', - enum: ['default', 'copyPaste', 'paste', 'selectWord'], + enum: ['default', 'copyPaste', 'paste', 'selectWord', 'nothing'], enumDescriptions: [ localize('terminal.integrated.rightClickBehavior.default', "Show the context menu."), localize('terminal.integrated.rightClickBehavior.copyPaste', "Copy when there is a selection, otherwise paste."), localize('terminal.integrated.rightClickBehavior.paste', "Paste on right click."), - localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu.") + localize('terminal.integrated.rightClickBehavior.selectWord', "Select the word under the cursor and show the context menu."), + localize('terminal.integrated.rightClickBehavior.nothing', "Do nothing and pass event to terminal.") ], default: isMacintosh ? 'selectWord' : isWindows ? 'copyPaste' : 'default', description: localize('terminal.integrated.rightClickBehavior', "Controls how terminal reacts to right click.") diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index bbddbae1f31..b1922c5cd52 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/** + * This module contains utility functions related to the environment, cwd and paths. + */ + import * as path from 'vs/base/common/path'; import { URI as Uri } from 'vs/base/common/uri'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -11,12 +15,6 @@ import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; import { ILogService } from 'vs/platform/log/common/log'; import { IShellLaunchConfig, ITerminalEnvironment, TerminalSettingId, TerminalSettingPrefix } from 'vs/platform/terminal/common/terminal'; import { IProcessEnvironment, isWindows, locale, OperatingSystem, OS, platform, Platform } from 'vs/base/common/platform'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { format } from 'vs/base/common/strings'; - -/** - * This module contains utility functions related to the environment, cwd and paths. - */ export function mergeEnvironments(parent: IProcessEnvironment, other: ITerminalEnvironment | undefined): void { if (!other) { @@ -401,135 +399,3 @@ export function createTerminalEnvironment( } return env; } -export enum ShellIntegrationExecutable { - WindowsPwsh = 'windows-pwsh', - WindowsPwshLogin = 'windows-pwsh-login', - Pwsh = 'pwsh', - PwshLogin = 'pwsh-login', - Zsh = 'zsh', - ZshLogin = 'zsh-login', - Bash = 'bash' -} - -export const shellIntegrationArgs: Map = new Map(); -shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwsh, ['-noexit', ' -command', '. \"${execInstallFolder}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1\"{0}']); -shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwshLogin, ['-l', '-noexit', ' -command', '. \"${execInstallFolder}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1\"{0}']); -shellIntegrationArgs.set(ShellIntegrationExecutable.Pwsh, ['-noexit', '-command', '. "${execInstallFolder}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"{0}']); -shellIntegrationArgs.set(ShellIntegrationExecutable.PwshLogin, ['-l', '-noexit', '-command', '. "${execInstallFolder}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"']); -shellIntegrationArgs.set(ShellIntegrationExecutable.Zsh, ['-i']); -shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']); -shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, ['--init-file', '${execInstallFolder}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh']); -const loginArgs = ['-login', '-l']; -const pwshImpliedArgs = ['-nol', '-nologo']; -export function injectShellIntegrationArgs( - logService: ILogService, - configurationService: IConfigurationService, - env: IProcessEnvironment, - enableShellIntegration: boolean, - shellLaunchConfig: IShellLaunchConfig, - os?: OperatingSystem -): { args: string | string[] | undefined; enableShellIntegration: boolean } { - // Shell integration arg injection is disabled when: - // - The global setting is disabled - // - There is no executable (not sure what script to run) - // - The terminal is used by a feature like tasks or debugging - if (!enableShellIntegration || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal) { - return { args: shellLaunchConfig.args, enableShellIntegration: false }; - } - - const originalArgs = shellLaunchConfig.args; - const shell = path.basename(shellLaunchConfig.executable).toLowerCase(); - let newArgs: string[] | undefined; - - if (os === OperatingSystem.Windows) { - if (shell === 'pwsh.exe') { - if (!originalArgs || arePwshImpliedArgs(originalArgs)) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwsh); - } else if (arePwshLoginArgs(originalArgs)) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwshLogin); - } else { - logService.warn(`Shell integration cannot be enabled when custom args ${originalArgs} are provided for ${shell} on Windows.`); - } - } - if (newArgs) { - const showWelcome = configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome); - const additionalArgs = showWelcome ? '' : ' -HideWelcome'; - newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array - newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], additionalArgs); - } - } else { - switch (shell) { - case 'bash': { - if (!originalArgs || originalArgs.length === 0) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); - } else if (areZshBashLoginArgs(originalArgs)) { - env['VSCODE_SHELL_LOGIN'] = '1'; - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); - } - const showWelcome = configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome); - if (!showWelcome) { - env['VSCODE_SHELL_HIDE_WELCOME'] = '1'; - } - break; - } - case 'pwsh': { - if (!originalArgs || arePwshImpliedArgs(originalArgs)) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Pwsh); - } else if (arePwshLoginArgs(originalArgs)) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.PwshLogin); - } - if (newArgs) { - const showWelcome = configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome); - const additionalArgs = showWelcome ? '' : ' -HideWelcome'; - newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array - newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], additionalArgs); - } - break; - } - case 'zsh': { - if (!originalArgs || originalArgs.length === 0) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh); - } else if (areZshBashLoginArgs(originalArgs)) { - newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin); - } else if (originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh) || originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin)) { - newArgs = originalArgs; - } - // Set _ZDOTDIR which will defer setting ZDOTDIR to the pty host - env['_ZDOTDIR'] = '1'; - const showWelcome = configurationService.getValue(TerminalSettingId.ShellIntegrationShowWelcome); - if (!showWelcome) { - env['VSCODE_SHELL_HIDE_WELCOME'] = '1'; - } - break; - } - } - if (!newArgs) { - logService.warn(`Shell integration cannot be enabled when custom args ${originalArgs} are provided for ${shell}.`); - } - } - return { args: newArgs || originalArgs, enableShellIntegration: newArgs !== undefined }; -} - -function arePwshLoginArgs(originalArgs: string | string[]): boolean { - if (typeof originalArgs === 'string') { - return loginArgs.includes(originalArgs.toLowerCase()); - } else { - return originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()) || - (originalArgs.length === 2 && - (((loginArgs.includes(originalArgs[0].toLowerCase())) || loginArgs.includes(originalArgs[1].toLowerCase()))) - && ((pwshImpliedArgs.includes(originalArgs[0].toLowerCase())) || pwshImpliedArgs.includes(originalArgs[1].toLowerCase()))); - } -} - -function arePwshImpliedArgs(originalArgs: string | string[]): boolean { - if (typeof originalArgs === 'string') { - return pwshImpliedArgs.includes(originalArgs.toLowerCase()); - } else { - return originalArgs.length === 0 || originalArgs?.length === 1 && pwshImpliedArgs.includes(originalArgs[0].toLowerCase()); - } -} - -function areZshBashLoginArgs(originalArgs: string | string[]): boolean { - return originalArgs === 'string' && loginArgs.includes(originalArgs.toLowerCase()) - || typeof originalArgs !== 'string' && originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()); -} diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index 689d383539d..f840fbf52d1 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -7,7 +7,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, IProcessProperty, IProcessPropertyMap, ProcessPropertyType, IProcessReadyEvent } from 'vs/platform/terminal/common/terminal'; -import { IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; +import { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; import { URI } from 'vs/base/common/uri'; /** @@ -36,6 +36,8 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { readonly onDidChangeProperty = this._onDidChangeProperty.event; private readonly _onProcessExit = this._register(new Emitter()); readonly onProcessExit = this._onProcessExit.event; + private readonly _onRestoreCommands = this._register(new Emitter()); + readonly onRestoreCommands = this._onRestoreCommands.event; constructor( readonly id: number, @@ -139,6 +141,10 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { this._inReplay = false; } + if (e.commands) { + this._onRestoreCommands.fire(e.commands); + } + // remove size override this._onDidChangeProperty.fire({ type: ProcessPropertyType.OverrideDimensions, value: undefined }); } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index 74fb08b2f02..c38b20757e8 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -14,7 +14,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IProcessPropertyMap, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalSettingId, TitleEventSource } from 'vs/platform/terminal/common/terminal'; +import { IProcessPropertyMap, IShellLaunchConfig, ITerminalChildProcess, ITerminalEnvironment, ITerminalProcessOptions, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalSettingId, TitleEventSource } from 'vs/platform/terminal/common/terminal'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ILocalPtyService } from 'vs/platform/terminal/electron-sandbox/terminal'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -146,11 +146,11 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke rows: number, unicodeVersion: '6' | '11', env: IProcessEnvironment, - windowsEnableConpty: boolean, + options: ITerminalProcessOptions, shouldPersist: boolean ): Promise { const executableEnv = await this._shellEnvironmentService.getShellEnv(); - const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, windowsEnableConpty, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName()); + const id = await this._localPtyService.createProcess(shellLaunchConfig, cwd, cols, rows, unicodeVersion, env, executableEnv, options, shouldPersist, this._getWorkspaceId(), this._getWorkspaceName()); const pty = this._instantiationService.createInstance(LocalPty, id, shouldPersist); this._ptys.set(id, pty); return pty; @@ -226,7 +226,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke // environment for (const state of parsed) { const freshEnv = await this._resolveEnvironmentForRevive(variableResolver, state.shellLaunchConfig); - state.processLaunchOptions.env = freshEnv; + state.processLaunchConfig.env = freshEnv; } await this._localPtyService.reviveTerminalProcesses(parsed, Intl.DateTimeFormat().resolvedOptions().locale); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index f43dcb9c6f1..c2e7d77cfa9 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -6,9 +6,9 @@ import { deepStrictEqual, ok } from 'assert'; import { timeout } from 'vs/base/common/async'; import { Terminal } from 'xterm'; -import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability'; +import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { NullLogService } from 'vs/platform/log/common/log'; -import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; async function writeP(terminal: Terminal, data: string): Promise { return new Promise((resolve, reject) => { diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts index 05ae39dd9fe..9475f8a1828 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts @@ -5,7 +5,7 @@ import { deepStrictEqual } from 'assert'; import { timeout } from 'vs/base/common/async'; -import { PartialCommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability'; +import { PartialCommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/partialCommandDetectionCapability'; import { IMarker, Terminal } from 'xterm'; import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; @@ -43,11 +43,11 @@ suite('PartialCommandDetectionCapability', () => { test('should not add commands when the cursor position is too close to the left side', async () => { assertCommands([]); - xterm._core._onKey.fire({ key: '\x0d' }); + xterm._core._onData.fire('\x0d'); await writeP(xterm, '\r\n'); assertCommands([]); await writeP(xterm, 'a'); - xterm._core._onKey.fire({ key: '\x0d' }); + xterm._core._onData.fire('\x0d'); await writeP(xterm, '\r\n'); assertCommands([]); }); @@ -55,11 +55,11 @@ suite('PartialCommandDetectionCapability', () => { test('should add commands when the cursor position is not too close to the left side', async () => { assertCommands([]); await writeP(xterm, 'ab'); - xterm._core._onKey.fire({ key: '\x0d' }); + xterm._core._onData.fire('\x0d'); await writeP(xterm, '\r\n\r\n'); assertCommands([0]); await writeP(xterm, 'cd'); - xterm._core._onKey.fire({ key: '\x0d' }); + xterm._core._onData.fire('\x0d'); await writeP(xterm, '\r\n'); assertCommands([0, 2]); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts index 5c3d859db1d..deb033af52f 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual } from 'assert'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { TerminalCapabilityStore, TerminalCapabilityStoreMultiplexer } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore, TerminalCapabilityStoreMultiplexer } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; suite('TerminalCapabilityStore', () => { let store: TerminalCapabilityStore; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts index 903d737f2e6..e37c554e6ef 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkManager.test.ts @@ -17,7 +17,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; -import { ITerminalCapabilityImplMap, ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ITerminalCapabilityImplMap, ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { ITerminalConfiguration, ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal'; import { TestViewDescriptorService } from 'vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts index 7b31b104f2f..b6efab0951f 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLinkOpeners.test.ts @@ -14,11 +14,11 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability'; +import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links'; import { TerminalLocalFileLinkOpener, TerminalLocalFolderInWorkspaceLinkOpener, TerminalSearchLinkOpener } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkOpeners'; -import { TerminalCapability, ITerminalCommand, IXtermMarker } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapability, ITerminalCommand, IXtermMarker } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLocalLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLocalLinkDetector.test.ts index a39e7867016..8ac3607d6e8 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalLocalLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalLocalLinkDetector.test.ts @@ -10,7 +10,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITerminalSimpleLink, TerminalBuiltinLinkType } from 'vs/workbench/contrib/terminal/browser/links/links'; import { TerminalLocalLinkDetector } from 'vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { assertLinkHelper, resolveLinkForTest } from 'vs/workbench/contrib/terminal/test/browser/links/linkTestUtils'; import { Terminal } from 'xterm'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts deleted file mode 100644 index bcf23f57aeb..00000000000 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalCommandTracker.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { Terminal } from 'xterm'; -import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/xterm/commandTrackerAddon'; -import { isWindows } from 'vs/base/common/platform'; -import { IXtermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; -import { timeout } from 'vs/base/common/async'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; -import { PartialCommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/partialCommandDetectionCapability'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; - -interface TestTerminal extends Terminal { - _core: IXtermCore; -} - -const ROWS = 10; -const COLS = 10; - -async function writeP(terminal: TestTerminal, data: string): Promise { - return new Promise((resolve, reject) => { - const failTimeout = timeout(2000); - failTimeout.then(() => reject('Writing to xterm is taking longer than 2 seconds')); - terminal.write(data, () => { - failTimeout.cancel(); - resolve(); - }); - }); -} - -suite('Workbench - TerminalCommandTracker', function () { - let xterm: TestTerminal; - let commandTracker: CommandTrackerAddon; - let store: TerminalCapabilityStore; - - // These tests are flaky on GH actions as sometimes they are particularly slow and timeout - // on the await writeP calls. These have been reduced but the timeout is increased to try - // catch edge cases. - this.timeout(20000); - - setup(async function () { - xterm = (new Terminal({ - cols: COLS, - rows: ROWS - })); - // Fill initial viewport - let data = ''; - for (let i = 0; i < ROWS - 1; i++) { - data += `${i}\n`; - } - await writeP(xterm, data); - store = new TerminalCapabilityStore(); - commandTracker = new CommandTrackerAddon(store); - store.add(TerminalCapability.PartialCommandDetection, new PartialCommandDetectionCapability(xterm)); - xterm.loadAddon(commandTracker); - }); - - suite('Command tracking', () => { - test('should track commands when the prompt is of sufficient size', async () => { - assert.strictEqual(xterm.markers.length, 0); - await writeP(xterm, '\x1b[3G'); // Move cursor to column 3 - xterm._core._onKey.fire({ key: '\x0d' }); - assert.strictEqual(xterm.markers.length, 1); - }); - test('should not track commands when the prompt is too small', async () => { - assert.strictEqual(xterm.markers.length, 0); - await writeP(xterm, '\x1b[2G'); // Move cursor to column 2 - xterm._core._onKey.fire({ key: '\x0d' }); - assert.strictEqual(xterm.markers.length, 0); - }); - }); - - suite('Commands', () => { - let container: HTMLElement; - setup(() => { - container = document.createElement('div'); - document.body.appendChild(container); - xterm.open(container); - }); - teardown(() => { - document.body.removeChild(container); - }); - test('should scroll to the next and previous commands', async () => { - await writeP(xterm, '\x1b[3G'); // Move cursor to column 3 - xterm._core._onKey.fire({ key: '\x0d' }); // Mark line #10 - assert.strictEqual(xterm.markers[0].line, 9); - - await writeP(xterm, `\r\n`.repeat(20)); - assert.strictEqual(xterm.buffer.active.baseY, 20); - assert.strictEqual(xterm.buffer.active.viewportY, 20); - - // Scroll to marker - commandTracker.scrollToPreviousCommand(); - assert.strictEqual(xterm.buffer.active.viewportY, 9); - - // Scroll to top boundary - commandTracker.scrollToPreviousCommand(); - assert.strictEqual(xterm.buffer.active.viewportY, 0); - - // Scroll to marker - commandTracker.scrollToNextCommand(); - assert.strictEqual(xterm.buffer.active.viewportY, 9); - - // Scroll to bottom boundary - commandTracker.scrollToNextCommand(); - assert.strictEqual(xterm.buffer.active.viewportY, 20); - }); - test('should select to the next and previous commands', async () => { - await writeP(xterm, - '\r0' + - '\n\r1' + - '\x1b[3G' // Move cursor to column 3 - ); - xterm._core._onKey.fire({ key: '\x0d' }); // Mark line - assert.strictEqual(xterm.markers[0].line, 10); - await writeP(xterm, - '\n\r2' + - '\x1b[3G' // Move cursor to column 3 - ); - xterm._core._onKey.fire({ key: '\x0d' }); // Mark line - assert.strictEqual(xterm.markers[1].line, 11); - await writeP(xterm, '\n\r3'); - - assert.strictEqual(xterm.buffer.active.baseY, 3); - assert.strictEqual(xterm.buffer.active.viewportY, 3); - - assert.strictEqual(xterm.getSelection(), ''); - commandTracker.selectToPreviousCommand(); - assert.strictEqual(xterm.getSelection(), '2'); - commandTracker.selectToPreviousCommand(); - assert.strictEqual(xterm.getSelection(), isWindows ? '1\r\n2' : '1\n2'); - commandTracker.selectToNextCommand(); - assert.strictEqual(xterm.getSelection(), '2'); - commandTracker.selectToNextCommand(); - assert.strictEqual(xterm.getSelection(), isWindows ? '\r\n' : '\n'); - }); - test('should select to the next and previous lines & commands', async () => { - await writeP(xterm, - '\r0' + - '\n\r1' + - '\x1b[3G' // Move cursor to column 3 - ); - xterm._core._onKey.fire({ key: '\x0d' }); // Mark line - assert.strictEqual(xterm.markers[0].line, 10); - await writeP(xterm, - '\n\r2' + - '\x1b[3G' // Move cursor to column 3 - ); - xterm._core._onKey.fire({ key: '\x0d' }); // Mark line - assert.strictEqual(xterm.markers[1].line, 11); - await writeP(xterm, '\n\r3'); - - assert.strictEqual(xterm.buffer.active.baseY, 3); - assert.strictEqual(xterm.buffer.active.viewportY, 3); - - assert.strictEqual(xterm.getSelection(), ''); - commandTracker.selectToPreviousLine(); - assert.strictEqual(xterm.getSelection(), '2'); - commandTracker.selectToNextLine(); - commandTracker.selectToNextLine(); - assert.strictEqual(xterm.getSelection(), '3'); - commandTracker.selectToPreviousCommand(); - commandTracker.selectToPreviousCommand(); - commandTracker.selectToNextLine(); - assert.strictEqual(xterm.getSelection(), '2'); - commandTracker.selectToPreviousCommand(); - assert.strictEqual(xterm.getSelection(), isWindows ? '1\r\n2' : '1\n2'); - commandTracker.selectToPreviousLine(); - assert.strictEqual(xterm.getSelection(), isWindows ? '0\r\n1\r\n2' : '0\n1\n2'); - }); - }); -}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index c8b9d09eef0..9d89281d10d 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -17,8 +17,8 @@ import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/termina import { ProcessState } from 'vs/workbench/contrib/terminal/common/terminal'; import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Schemas } from 'vs/base/common/network'; function createInstance(partial?: Partial): Pick { diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts index f6b8fbc3eb9..80802218586 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts @@ -8,14 +8,16 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { ITerminalCommand } from 'vs/workbench/contrib/terminal/common/terminal'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IDecoration, IDecorationOptions, Terminal } from 'xterm'; -import { TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; -import { CommandDetectionCapability } from 'vs/workbench/contrib/terminal/browser/capabilities/commandDetectionCapability'; +import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; +import { CommandDetectionCapability } from 'vs/platform/terminal/common/capabilities/commandDetectionCapability'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; class TestTerminal extends Terminal { override registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined { @@ -38,6 +40,7 @@ suite('DecorationAddon', () => { hover: { delay: 5 } } }); + instantiationService.stub(IThemeService, new TestThemeService()); xterm = new TestTerminal({ cols: 80, rows: 30 diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index 7aefd01be53..8e8740ea388 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -7,8 +7,8 @@ import { Terminal } from 'xterm'; import { strictEqual } from 'assert'; import { timeout } from 'vs/base/common/async'; import * as sinon from 'sinon'; -import { ShellIntegrationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/shellIntegrationAddon'; -import { ITerminalCapabilityStore, TerminalCapability } from 'vs/workbench/contrib/terminal/common/capabilities/capabilities'; +import { ShellIntegrationAddon } from 'vs/platform/terminal/common/xterm/shellIntegrationAddon'; +import { ITerminalCapabilityStore, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index 1313c2cccf0..c9bbe8dedaf 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -24,7 +24,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { isSafari } from 'vs/base/browser/browser'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; -import { TerminalCapabilityStore } from 'vs/workbench/contrib/terminal/common/capabilities/terminalCapabilityStore'; +import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ContextMenuService } from 'vs/platform/contextview/browser/contextMenuService'; diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index f0737aa3e58..63c258c9b8e 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -31,7 +31,7 @@ function getMockTheme(type: ColorScheme): IColorTheme { suite('Workbench - TerminalColorRegistry', () => { test('hc colors', function () { - const theme = getMockTheme(ColorScheme.HIGH_CONTRAST); + const theme = getMockTheme(ColorScheme.HIGH_CONTRAST_DARK); const colors = ansiColorIdentifiers.map(colorId => Color.Format.CSS.formatHexA(theme.getColor(colorId)!, true)); assert.deepStrictEqual(colors, [ diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index 398c2caf921..ca148531c12 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -3,14 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI as Uri } from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { addTerminalEnvironmentKeys, mergeEnvironments, getCwd, getDefaultShell, getLangEnvVariable, shouldSetLangEnvVariable, injectShellIntegrationArgs, shellIntegrationArgs, ShellIntegrationExecutable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { IProcessEnvironment, isWindows, OperatingSystem, OS, Platform } from 'vs/base/common/platform'; import { deepStrictEqual, strictEqual } from 'assert'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { terminalProfileArgsMatch } from 'vs/platform/terminal/common/terminalProfiles'; -import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { isWindows, Platform } from 'vs/base/common/platform'; +import { URI as Uri } from 'vs/base/common/uri'; +import { addTerminalEnvironmentKeys, getCwd, getDefaultShell, getLangEnvVariable, mergeEnvironments, shouldSetLangEnvVariable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; suite('Workbench - TerminalEnvironment', () => { suite('addTerminalEnvironmentKeys', () => { @@ -251,253 +248,4 @@ suite('Workbench - TerminalEnvironment', () => { strictEqual(shell3, 'automationShell', 'automationShell was true and specified in settings'); }); }); - - suite('injectShellIntegrationArgs', () => { - const env = {} as IProcessEnvironment; - const logService = new NullLogService(); - const configurationService = new TestConfigurationService(); - let shellIntegrationEnabled = true; - - suite('should not enable', () => { - const executable = OS ? 'pwsh.exe' : 'pwsh'; - test('when isFeatureTerminal or when no executable is provided', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, OS); - terminalProfileArgsMatch(args, ['-l', '-NoLogo']); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { args: [] }, OS)); - terminalProfileArgsMatch(args, []); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - - suite('pwsh', () => { - - let executable = OS ? 'pwsh.exe' : 'pwsh'; - - suite('should override args', () => { - const expectedArgs = OS ? shellIntegrationArgs.get(ShellIntegrationExecutable.Pwsh) : shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwsh); - test('when undefined, [], empty string, or empty string in array', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [''] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - suite('when no logo', () => { - test('array - case insensitive', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-NoLogo'] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-NOLOGO'] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-nol'] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-NOL'] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('string - case insensitive', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-NoLogo' }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-NOLOGO' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-nol' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-Nol' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('regardless of executable case', () => { - executable = OS ? 'pwSh.exe' : 'PWsh'; - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-NoLogo' }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-NOLOGO' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-nol' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-Nol' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - executable = OS ? 'pwsh.exe' : 'pwsh'; - }); - }); - }); - suite('should incorporate login arg', () => { - const expectedArgs = OS ? shellIntegrationArgs.get(ShellIntegrationExecutable.PwshLogin) : shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwshLogin); - test('when array contains no logo and login', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l', '-NoLogo'] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - suite('should not modify args', () => { - shellIntegrationEnabled = false; - test('when shell integration is disabled', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - strictEqual(args, '-l'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - strictEqual(args, undefined); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom array entry', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l', '-NoLogo', '-i'] }, OS); - terminalProfileArgsMatch(args, ['-l', '-NoLogo', '-i']); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-i' }, OS); - terminalProfileArgsMatch(args, '-i'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - }); - - if (OS !== OperatingSystem.Windows) { - suite('zsh', () => { - - let executable = 'zsh'; - - suite('should override args', () => { - const expectedArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh); - test('when undefined, [], empty string, or empty string in array', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [''] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - suite('should incorporate login arg', () => { - const expectedArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin); - test('when array', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l'] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('regardless of executable case', () => { - executable = 'ZSH'; - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - executable = 'zsh'; - }); - }); - suite('should not modify args', () => { - shellIntegrationEnabled = false; - test('when shell integration is disabled', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - strictEqual(args, '-l'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - strictEqual(args, undefined); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom array entry', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l', '-i'] }, OS); - terminalProfileArgsMatch(args, ['-l', '-i']); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-i' }, OS); - terminalProfileArgsMatch(args, '-i'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - }); - }); - suite('bash', () => { - let executable = 'bash'; - - suite('should override args', () => { - const expectedArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); - test('when undefined, [], empty string, or empty string in array', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [''] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [] }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '' }, OS)); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('regardless of executable case', () => { - executable = 'BasH'; - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: [''] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - suite('should set login env variable and not modify args', () => { - const expectedArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash); - test('when array', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l'] }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - terminalProfileArgsMatch(args, expectedArgs); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - suite('should not modify args', () => { - shellIntegrationEnabled = false; - test('when shell integration is disabled', () => { - let { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-l' }, OS); - strictEqual(args, '-l'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - ({ args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: undefined }, OS)); - strictEqual(args, undefined); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom array entry', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: ['-l', '-i'] }, OS); - terminalProfileArgsMatch(args, ['-l', '-i']); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - test('when custom string', () => { - const { args, enableShellIntegration } = injectShellIntegrationArgs(logService, configurationService, env, shellIntegrationEnabled, { executable, args: '-i' }, OS); - terminalProfileArgsMatch(args, '-i'); - strictEqual(enableShellIntegration, shellIntegrationEnabled); - }); - }); - }); - }); - } - }); }); diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 0adef01ac92..7758973aa12 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -19,6 +19,7 @@ .test-output-peek-tree .test-peek-item { display: flex; align-items: center; + color: var(--vscode-editor-foreground); } .test-output-peek-tree .monaco-list-row .monaco-action-bar, diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 63c669a9e8f..8230419e933 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -29,7 +29,7 @@ import { IActionableTestTreeElement, TestItemTreeElement } from 'vs/workbench/co import * as icons from 'vs/workbench/contrib/testing/browser/icons'; import type { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService'; -import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { InternalTestItem, ITestRunProfile, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -65,10 +65,9 @@ const enum ActionOrder { const hasAnyTestProvider = ContextKeyGreaterExpr.create(TestingContextKeys.providerCount.key, 0); export class HideTestAction extends Action2 { - public static readonly ID = 'testing.hideTest'; constructor() { super({ - id: HideTestAction.ID, + id: TestCommandId.HideTestAction, title: localize('hideTest', 'Hide Test'), menu: { id: MenuId.TestItem, @@ -90,10 +89,9 @@ export class HideTestAction extends Action2 { } export class UnhideTestAction extends Action2 { - public static readonly ID = 'testing.unhideTest'; constructor() { super({ - id: UnhideTestAction.ID, + id: TestCommandId.UnhideTestAction, title: localize('unhideTest', 'Unhide Test'), menu: { id: MenuId.TestItem, @@ -129,10 +127,9 @@ const testItemInlineAndInContext = (order: ActionOrder, when?: ContextKeyExpress ]; export class DebugAction extends Action2 { - public static readonly ID = 'testing.debug'; constructor() { super({ - id: DebugAction.ID, + id: TestCommandId.DebugAction, title: localize('debug test', 'Debug Test'), icon: icons.testingDebugIcon, menu: testItemInlineAndInContext(ActionOrder.Debug, TestingContextKeys.hasDebuggableTests.isEqualTo(true)), @@ -148,10 +145,9 @@ export class DebugAction extends Action2 { } export class RunUsingProfileAction extends Action2 { - public static readonly ID = 'testing.runUsing'; constructor() { super({ - id: RunUsingProfileAction.ID, + id: TestCommandId.RunUsingProfileAction, title: localize('testing.runUsing', 'Execute Using Profile...'), icon: icons.testingDebugIcon, menu: { @@ -190,10 +186,9 @@ export class RunUsingProfileAction extends Action2 { } export class RunAction extends Action2 { - public static readonly ID = 'testing.run'; constructor() { super({ - id: RunAction.ID, + id: TestCommandId.RunAction, title: localize('run test', 'Run Test'), icon: icons.testingRunIcon, menu: testItemInlineAndInContext(ActionOrder.Run, TestingContextKeys.hasRunnableTests.isEqualTo(true)), @@ -212,10 +207,9 @@ export class RunAction extends Action2 { } export class SelectDefaultTestProfiles extends Action2 { - public static readonly ID = 'testing.selectDefaultTestProfiles'; constructor() { super({ - id: SelectDefaultTestProfiles.ID, + id: TestCommandId.SelectDefaultTestProfiles, title: localize('testing.selectDefaultTestProfiles', 'Select Default Profile'), icon: icons.testingUpdateProfiles, category, @@ -238,10 +232,9 @@ export class SelectDefaultTestProfiles extends Action2 { } export class ConfigureTestProfilesAction extends Action2 { - public static readonly ID = 'testing.configureProfile'; constructor() { super({ - id: ConfigureTestProfilesAction.ID, + id: TestCommandId.ConfigureTestProfilesAction, title: localize('testing.configureProfile', 'Configure Test Profiles'), icon: icons.testingUpdateProfiles, f1: true, @@ -302,11 +295,10 @@ abstract class ExecuteSelectedAction extends ViewAction { } export class RunSelectedAction extends ExecuteSelectedAction { - public static readonly ID = 'testing.runSelected'; constructor() { super({ - id: RunSelectedAction.ID, + id: TestCommandId.RunSelectedAction, title: localize('runSelectedTests', 'Run Tests'), icon: icons.testingRunAllIcon, }, TestRunProfileBitset.Run); @@ -314,11 +306,10 @@ export class RunSelectedAction extends ExecuteSelectedAction { } export class DebugSelectedAction extends ExecuteSelectedAction { - public static readonly ID = 'testing.debugSelected'; constructor() { super({ - id: DebugSelectedAction.ID, + id: TestCommandId.DebugSelectedAction, title: localize('debugSelectedTests', 'Debug Tests'), icon: icons.testingDebugAllIcon, }, TestRunProfileBitset.Debug); @@ -362,11 +353,10 @@ abstract class RunOrDebugAllTestsAction extends Action2 { } export class RunAllAction extends RunOrDebugAllTestsAction { - public static readonly ID = 'testing.runAll'; constructor() { super( { - id: RunAllAction.ID, + id: TestCommandId.RunAllAction, title: localize('runAllTests', 'Run All Tests'), icon: icons.testingRunAllIcon, keybinding: { @@ -381,11 +371,10 @@ export class RunAllAction extends RunOrDebugAllTestsAction { } export class DebugAllAction extends RunOrDebugAllTestsAction { - public static readonly ID = 'testing.debugAll'; constructor() { super( { - id: DebugAllAction.ID, + id: TestCommandId.DebugAllAction, title: localize('debugAllTests', 'Debug All Tests'), icon: icons.testingDebugIcon, keybinding: { @@ -400,10 +389,9 @@ export class DebugAllAction extends RunOrDebugAllTestsAction { } export class CancelTestRunAction extends Action2 { - public static readonly ID = 'testing.cancelRun'; constructor() { super({ - id: CancelTestRunAction.ID, + id: TestCommandId.CancelTestRunAction, title: localize('testing.cancelRun', "Cancel Test Run"), icon: icons.testingCancelIcon, keybinding: { @@ -437,10 +425,9 @@ export class CancelTestRunAction extends Action2 { } export class TestingViewAsListAction extends ViewAction { - public static readonly ID = 'testing.viewAsList'; constructor() { super({ - id: TestingViewAsListAction.ID, + id: TestCommandId.TestingViewAsListAction, viewId: Testing.ExplorerViewId, title: localize('testing.viewAsList', "View as List"), toggled: TestingContextKeys.viewMode.isEqualTo(TestExplorerViewMode.List), @@ -462,10 +449,9 @@ export class TestingViewAsListAction extends ViewAction { } export class TestingViewAsTreeAction extends ViewAction { - public static readonly ID = 'testing.viewAsTree'; constructor() { super({ - id: TestingViewAsTreeAction.ID, + id: TestCommandId.TestingViewAsTreeAction, viewId: Testing.ExplorerViewId, title: localize('testing.viewAsTree', "View as Tree"), toggled: TestingContextKeys.viewMode.isEqualTo(TestExplorerViewMode.Tree), @@ -488,10 +474,9 @@ export class TestingViewAsTreeAction extends ViewAction { export class TestingSortByStatusAction extends ViewAction { - public static readonly ID = 'testing.sortByStatus'; constructor() { super({ - id: TestingSortByStatusAction.ID, + id: TestCommandId.TestingSortByStatusAction, viewId: Testing.ExplorerViewId, title: localize('testing.sortByStatus', "Sort by Status"), toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByStatus), @@ -513,10 +498,9 @@ export class TestingSortByStatusAction extends ViewAction { } export class TestingSortByLocationAction extends ViewAction { - public static readonly ID = 'testing.sortByLocation'; constructor() { super({ - id: TestingSortByLocationAction.ID, + id: TestCommandId.TestingSortByLocationAction, viewId: Testing.ExplorerViewId, title: localize('testing.sortByLocation', "Sort by Location"), toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByLocation), @@ -538,10 +522,9 @@ export class TestingSortByLocationAction extends ViewAction } export class TestingSortByDurationAction extends ViewAction { - public static readonly ID = 'testing.sortByDuration'; constructor() { super({ - id: TestingSortByDurationAction.ID, + id: TestCommandId.TestingSortByDurationAction, viewId: Testing.ExplorerViewId, title: localize('testing.sortByDuration', "Sort by Duration"), toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByDuration), @@ -563,10 +546,9 @@ export class TestingSortByDurationAction extends ViewAction } export class ShowMostRecentOutputAction extends Action2 { - public static readonly ID = 'testing.showMostRecentOutput'; constructor() { super({ - id: ShowMostRecentOutputAction.ID, + id: TestCommandId.ShowMostRecentOutputAction, title: localize('testing.showMostRecentOutput', "Show Output"), category, icon: Codicon.terminal, @@ -594,10 +576,9 @@ export class ShowMostRecentOutputAction extends Action2 { } export class CollapseAllAction extends ViewAction { - public static readonly ID = 'testing.collapseAll'; constructor() { super({ - id: CollapseAllAction.ID, + id: TestCommandId.CollapseAllAction, viewId: Testing.ExplorerViewId, title: localize('testing.collapseAll', "Collapse All Tests"), icon: Codicon.collapseAll, @@ -619,10 +600,9 @@ export class CollapseAllAction extends ViewAction { } export class ClearTestResultsAction extends Action2 { - public static readonly ID = 'testing.clearTestResults'; constructor() { super({ - id: ClearTestResultsAction.ID, + id: TestCommandId.ClearTestResultsAction, title: localize('testing.clearResults', "Clear All Results"), category, icon: Codicon.trash, @@ -649,10 +629,9 @@ export class ClearTestResultsAction extends Action2 { } export class GoToTest extends Action2 { - public static readonly ID = 'testing.editFocusedTest'; constructor() { super({ - id: GoToTest.ID, + id: TestCommandId.GoToTest, title: localize('testing.editFocusedTest', "Go to Test"), icon: Codicon.goToFile, menu: testItemInlineAndInContext(ActionOrder.GoToTest, TestingContextKeys.testItemHasUri.isEqualTo(true)), @@ -672,11 +651,10 @@ export class GoToTest extends Action2 { } abstract class ToggleAutoRun extends Action2 { - public static readonly ID = 'testing.toggleautoRun'; constructor(title: string, whenToggleIs: boolean) { super({ - id: ToggleAutoRun.ID, + id: TestCommandId.ToggleAutoRun, title, icon: icons.testingAutorunIcon, toggled: whenToggleIs === true ? ContextKeyExpr.true() : ContextKeyExpr.false(), @@ -788,10 +766,9 @@ abstract class ExecuteTestAtCursor extends Action2 { } export class RunAtCursor extends ExecuteTestAtCursor { - public static readonly ID = 'testing.runAtCursor'; constructor() { super({ - id: RunAtCursor.ID, + id: TestCommandId.RunAtCursor, title: localize('testing.runAtCursor', "Run Test at Cursor"), category, keybinding: { @@ -804,10 +781,9 @@ export class RunAtCursor extends ExecuteTestAtCursor { } export class DebugAtCursor extends ExecuteTestAtCursor { - public static readonly ID = 'testing.debugAtCursor'; constructor() { super({ - id: DebugAtCursor.ID, + id: TestCommandId.DebugAtCursor, title: localize('testing.debugAtCursor', "Debug Test at Cursor"), category, keybinding: { @@ -871,11 +847,10 @@ abstract class ExecuteTestsInCurrentFile extends Action2 { } export class RunCurrentFile extends ExecuteTestsInCurrentFile { - public static readonly ID = 'testing.runCurrentFile'; constructor() { super({ - id: RunCurrentFile.ID, + id: TestCommandId.RunCurrentFile, title: localize('testing.runCurrentFile', "Run Tests in Current File"), category, keybinding: { @@ -888,11 +863,10 @@ export class RunCurrentFile extends ExecuteTestsInCurrentFile { } export class DebugCurrentFile extends ExecuteTestsInCurrentFile { - public static readonly ID = 'testing.debugCurrentFile'; constructor() { super({ - id: DebugCurrentFile.ID, + id: TestCommandId.DebugCurrentFile, title: localize('testing.debugCurrentFile', "Debug Tests in Current File"), category, keybinding: { @@ -998,10 +972,9 @@ abstract class RunOrDebugLastRun extends RunOrDebugExtsByPath { } export class ReRunFailedTests extends RunOrDebugFailedTests { - public static readonly ID = 'testing.reRunFailTests'; constructor() { super({ - id: ReRunFailedTests.ID, + id: TestCommandId.ReRunFailedTests, title: localize('testing.reRunFailTests', "Rerun Failed Tests"), category, keybinding: { @@ -1020,10 +993,9 @@ export class ReRunFailedTests extends RunOrDebugFailedTests { } export class DebugFailedTests extends RunOrDebugFailedTests { - public static readonly ID = 'testing.debugFailTests'; constructor() { super({ - id: DebugFailedTests.ID, + id: TestCommandId.DebugFailedTests, title: localize('testing.debugFailTests', "Debug Failed Tests"), category, keybinding: { @@ -1042,10 +1014,9 @@ export class DebugFailedTests extends RunOrDebugFailedTests { } export class ReRunLastRun extends RunOrDebugLastRun { - public static readonly ID = 'testing.reRunLastRun'; constructor() { super({ - id: ReRunLastRun.ID, + id: TestCommandId.ReRunLastRun, title: localize('testing.reRunLastRun', "Rerun Last Run"), category, keybinding: { @@ -1064,10 +1035,9 @@ export class ReRunLastRun extends RunOrDebugLastRun { } export class DebugLastRun extends RunOrDebugLastRun { - public static readonly ID = 'testing.debugLastRun'; constructor() { super({ - id: DebugLastRun.ID, + id: TestCommandId.DebugLastRun, title: localize('testing.debugLastRun', "Debug Last Run"), category, keybinding: { @@ -1086,10 +1056,9 @@ export class DebugLastRun extends RunOrDebugLastRun { } export class SearchForTestExtension extends Action2 { - public static readonly ID = 'testing.searchForTestExtension'; constructor() { super({ - id: SearchForTestExtension.ID, + id: TestCommandId.SearchForTestExtension, title: localize('testing.searchForTestExtension', "Search for Test Extension"), }); } @@ -1103,15 +1072,14 @@ export class SearchForTestExtension extends Action2 { } export class OpenOutputPeek extends Action2 { - public static readonly ID = 'testing.openOutputPeek'; constructor() { super({ - id: OpenOutputPeek.ID, + id: TestCommandId.OpenOutputPeek, title: localize('testing.openOutputPeek', "Peek Output"), category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyM), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyM), }, menu: { id: MenuId.CommandPalette, @@ -1126,10 +1094,9 @@ export class OpenOutputPeek extends Action2 { } export class ToggleInlineTestOutput extends Action2 { - public static readonly ID = 'testing.toggleInlineTestOutput'; constructor() { super({ - id: ToggleInlineTestOutput.ID, + id: TestCommandId.ToggleInlineTestOutput, title: localize('testing.toggleInlineTestOutput', "Toggle Inline Test Output"), category, keybinding: { @@ -1176,16 +1143,15 @@ const refreshMenus = (whenIsRefreshing: boolean): IAction2Options['menu'] => [ ]; export class RefreshTestsAction extends Action2 { - public static readonly ID = 'testing.refreshTests'; constructor() { super({ - id: RefreshTestsAction.ID, + id: TestCommandId.RefreshTestsAction, title: localize('testing.refreshTests', "Refresh Tests"), category, icon: icons.testingRefreshTests, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyCode.KeyR), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.Semicolon, KeyMod.CtrlCmd | KeyCode.KeyR), when: TestingContextKeys.canRefreshTests.isEqualTo(true), }, menu: refreshMenus(false), @@ -1213,10 +1179,9 @@ export class RefreshTestsAction extends Action2 { } export class CancelTestRefreshAction extends Action2 { - public static readonly ID = 'testing.cancelTestRefresh'; constructor() { super({ - id: CancelTestRefreshAction.ID, + id: TestCommandId.CancelTestRefreshAction, title: localize('testing.cancelTestRefresh', "Cancel Test Refresh"), category, icon: icons.testingCancelRefreshTests, diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index 43bf2f25f81..d50f2b5279a 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -18,7 +18,7 @@ import { IProgressService } from 'vs/platform/progress/common/progress'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { REVEAL_IN_EXPLORER_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { TestingDecorations, TestingDecorationService } from 'vs/workbench/contrib/testing/browser/testingDecorations'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; @@ -27,7 +27,7 @@ import { ITestingOutputTerminalService, TestingOutputTerminalService } from 'vs/ import { ITestingProgressUiService, TestingProgressTrigger, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; -import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { TestCommandId, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { ITestItem, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; import { TestId, TestPosition } from 'vs/workbench/contrib/testing/common/testId'; @@ -42,7 +42,7 @@ import { ITestResultStorage, TestResultStorage } from 'vs/workbench/contrib/test import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { TestService } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { allTestActions, discoverAndRunTests, SearchForTestExtension } from './testExplorerActions'; +import { allTestActions, discoverAndRunTests } from './testExplorerActions'; import './testingConfigurationUi'; registerSingleton(ITestService, TestService, true); @@ -80,7 +80,7 @@ viewsRegistry.registerViewWelcomeContent(Testing.ExplorerViewId, { }); viewsRegistry.registerViewWelcomeContent(Testing.ExplorerViewId, { - content: '[' + localize('searchForAdditionalTestExtensions', "Install Additional Test Extensions...") + `](command:${SearchForTestExtension.ID})`, + content: '[' + localize('searchForAdditionalTestExtensions', "Install Additional Test Extensions...") + `](command:${TestCommandId.SearchForTestExtension})`, order: 10 }); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index 928bd786a5e..546db017969 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -853,7 +853,7 @@ class RunSingleTestDecoration extends RunTestDecoration implements ITestDecorati } } -const lineBreakRe = /\r?\n\s*/; +const lineBreakRe = /\r?\n\s*/g; class TestMessageDecoration implements ITestDecoration { public static readonly inlineClassName = 'test-message-inline-content'; diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index 23832ee3205..31d4e1eec73 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -19,7 +19,7 @@ import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage' import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { attachSuggestEnabledInputBoxStyler, ContextScopedSuggestEnabledInputWithHistory, SuggestEnabledInputWithHistory, SuggestResultsProvider } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; -import { Testing } from 'vs/workbench/contrib/testing/common/constants'; +import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { denamespaceTestTag } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; @@ -235,7 +235,7 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { registerAction2(class extends Action2 { constructor() { super({ - id: Testing.FilterActionId, + id: TestCommandId.FilterAction, title: localize('filter', "Filter"), }); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index e28e0d3697a..88ad930b4bd 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -53,11 +53,10 @@ import { ByNameTestItemElement, HierarchicalByNameProjection } from 'vs/workbenc import { ITestTreeProjection, TestExplorerTreeElement, TestItemTreeElement, TestTreeErrorMessage } from 'vs/workbench/contrib/testing/browser/explorerProjections/index'; import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay'; import * as icons from 'vs/workbench/contrib/testing/browser/icons'; -import { ConfigureTestProfilesAction, DebugSelectedAction, RunSelectedAction, SelectDefaultTestProfiles } from 'vs/workbench/contrib/testing/browser/testExplorerActions'; import { TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { labelForTestInState, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; +import { labelForTestInState, TestCommandId, TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { InternalTestItem, ITestRunProfile, TestItemExpandState, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection'; import { ITestExplorerFilterState, TestExplorerFilterState, TestFilterTerm } from 'vs/workbench/contrib/testing/common/testExplorerFilterState'; @@ -236,11 +235,11 @@ export class TestingExplorerView extends ViewPane { /** @override */ public override getActionViewItem(action: IAction): IActionViewItem | undefined { switch (action.id) { - case Testing.FilterActionId: + case TestCommandId.FilterAction: return this.filter = this.instantiationService.createInstance(TestingExplorerFilter, action); - case RunSelectedAction.ID: + case TestCommandId.RunSelectedAction: return this.getRunGroupDropdown(TestRunProfileBitset.Run, action); - case DebugSelectedAction.ID: + case TestCommandId.DebugSelectedAction: return this.getRunGroupDropdown(TestRunProfileBitset.Debug, action); default: return super.getActionViewItem(action); @@ -302,7 +301,7 @@ export class TestingExplorerView extends ViewPane { localize('selectDefaultConfigs', 'Select Default Profile'), undefined, undefined, - () => this.commandService.executeCommand(SelectDefaultTestProfiles.ID, group), + () => this.commandService.executeCommand(TestCommandId.SelectDefaultTestProfiles, group), )); } @@ -312,7 +311,7 @@ export class TestingExplorerView extends ViewPane { localize('configureTestProfiles', 'Configure Test Profiles'), undefined, undefined, - () => this.commandService.executeCommand(ConfigureTestProfilesAction.ID, group), + () => this.commandService.executeCommand(TestCommandId.ConfigureTestProfilesAction, group), )); } @@ -357,7 +356,7 @@ export class TestingExplorerView extends ViewPane { actionViewItemProvider: action => this.getActionViewItem(action), triggerKeys: { keyDown: false, keys: [] }, }); - bar.push(new Action(Testing.FilterActionId)); + bar.push(new Action(TestCommandId.FilterAction)); bar.getContainer().classList.add('testing-filter-action-bar'); return bar; } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 5615bdce044..67897613b3c 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -1738,7 +1738,7 @@ export class ToggleTestingPeekHistory extends EditorAction2 { }], keybinding: { weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyH, + primary: KeyMod.Alt | KeyCode.KeyH, when: TestingContextKeys.isPeekVisible.isEqualTo(true), }, }); diff --git a/src/vs/workbench/contrib/testing/browser/theme.ts b/src/vs/workbench/contrib/testing/browser/theme.ts index 31eda025bb9..02c9577f04f 100644 --- a/src/vs/workbench/contrib/testing/browser/theme.ts +++ b/src/vs/workbench/contrib/testing/browser/theme.ts @@ -13,55 +13,64 @@ import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/c export const testingColorIconFailed = registerColor('testing.iconFailed', { dark: '#f14c4c', light: '#f14c4c', - hc: '#f14c4c' + hcDark: '#f14c4c', + hcLight: '#B5200D' }, localize('testing.iconFailed', "Color for the 'failed' icon in the test explorer.")); export const testingColorIconErrored = registerColor('testing.iconErrored', { dark: '#f14c4c', light: '#f14c4c', - hc: '#f14c4c' + hcDark: '#f14c4c', + hcLight: '#B5200D' }, localize('testing.iconErrored', "Color for the 'Errored' icon in the test explorer.")); export const testingColorIconPassed = registerColor('testing.iconPassed', { dark: '#73c991', light: '#73c991', - hc: '#73c991' + hcDark: '#73c991', + hcLight: '#007100' }, localize('testing.iconPassed', "Color for the 'passed' icon in the test explorer.")); export const testingColorRunAction = registerColor('testing.runAction', { dark: testingColorIconPassed, light: testingColorIconPassed, - hc: testingColorIconPassed + hcDark: testingColorIconPassed, + hcLight: testingColorIconPassed }, localize('testing.runAction', "Color for 'run' icons in the editor.")); export const testingColorIconQueued = registerColor('testing.iconQueued', { dark: '#cca700', light: '#cca700', - hc: '#cca700' + hcDark: '#cca700', + hcLight: '#cca700' }, localize('testing.iconQueued', "Color for the 'Queued' icon in the test explorer.")); export const testingColorIconUnset = registerColor('testing.iconUnset', { dark: '#848484', light: '#848484', - hc: '#848484' + hcDark: '#848484', + hcLight: '#848484' }, localize('testing.iconUnset', "Color for the 'Unset' icon in the test explorer.")); export const testingColorIconSkipped = registerColor('testing.iconSkipped', { dark: '#848484', light: '#848484', - hc: '#848484' + hcDark: '#848484', + hcLight: '#848484' }, localize('testing.iconSkipped', "Color for the 'Skipped' icon in the test explorer.")); export const testingPeekBorder = registerColor('testing.peekBorder', { dark: editorErrorForeground, light: editorErrorForeground, - hc: contrastBorder, + hcDark: contrastBorder, + hcLight: contrastBorder }, localize('testing.peekBorder', 'Color of the peek view borders and arrow.')); export const testingPeekHeaderBackground = registerColor('testing.peekHeaderBackground', { dark: transparent(editorErrorForeground, 0.1), light: transparent(editorErrorForeground, 0.1), - hc: null, + hcDark: null, + hcLight: null }, localize('testing.peekBorder', 'Color of the peek view borders and arrow.')); export const testMessageSeverityColors: { @@ -73,24 +82,24 @@ export const testMessageSeverityColors: { [TestMessageType.Error]: { decorationForeground: registerColor( 'testing.message.error.decorationForeground', - { dark: editorErrorForeground, light: editorErrorForeground, hc: editorForeground }, + { dark: editorErrorForeground, light: editorErrorForeground, hcDark: editorForeground, hcLight: editorForeground }, localize('testing.message.error.decorationForeground', 'Text color of test error messages shown inline in the editor.') ), marginBackground: registerColor( 'testing.message.error.lineBackground', - { dark: new Color(new RGBA(255, 0, 0, 0.2)), light: new Color(new RGBA(255, 0, 0, 0.2)), hc: null }, + { dark: new Color(new RGBA(255, 0, 0, 0.2)), light: new Color(new RGBA(255, 0, 0, 0.2)), hcDark: null, hcLight: null }, localize('testing.message.error.marginBackground', 'Margin color beside error messages shown inline in the editor.') ), }, [TestMessageType.Info]: { decorationForeground: registerColor( 'testing.message.info.decorationForeground', - { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hc: transparent(editorForeground, 0.5) }, + { dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hcDark: transparent(editorForeground, 0.5), hcLight: transparent(editorForeground, 0.5) }, localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.') ), marginBackground: registerColor( 'testing.message.info.lineBackground', - { dark: null, light: null, hc: null }, + { dark: null, light: null, hcDark: null, hcLight: null }, localize('testing.message.info.marginBackground', 'Margin color beside info messages shown inline in the editor.') ), }, @@ -102,7 +111,7 @@ export const testStatesToIconColors: { [K in TestResultState]?: string } = { [TestResultState.Passed]: testingColorIconPassed, [TestResultState.Queued]: testingColorIconQueued, [TestResultState.Unset]: testingColorIconUnset, - [TestResultState.Skipped]: testingColorIconUnset, + [TestResultState.Skipped]: testingColorIconSkipped, }; diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 0dbce98843c..44511be68c4 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -12,7 +12,6 @@ export const enum Testing { ExplorerViewId = 'workbench.view.testing', OutputPeekContributionId = 'editor.contrib.testingOutputPeek', DecorationsContributionId = 'editor.contrib.testingDecorations', - FilterActionId = 'workbench.actions.treeView.testExplorer.filter', } export const enum TestExplorerViewMode { @@ -52,3 +51,42 @@ export const testConfigurationGroupNames: { [K in TestRunProfileBitset]: string [TestRunProfileBitset.Run]: localize('testGroup.run', 'Run'), [TestRunProfileBitset.Coverage]: localize('testGroup.coverage', 'Coverage'), }; + +export const enum TestCommandId { + CancelTestRefreshAction = 'testing.cancelTestRefresh', + CancelTestRunAction = 'testing.cancelRun', + ClearTestResultsAction = 'testing.clearTestResults', + CollapseAllAction = 'testing.collapseAll', + ConfigureTestProfilesAction = 'testing.configureProfile', + DebugAction = 'testing.debug', + DebugAllAction = 'testing.debugAll', + DebugAtCursor = 'testing.debugAtCursor', + DebugCurrentFile = 'testing.debugCurrentFile', + DebugFailedTests = 'testing.debugFailTests', + DebugLastRun = 'testing.debugLastRun', + DebugSelectedAction = 'testing.debugSelected', + FilterAction = 'workbench.actions.treeView.testExplorer.filter', + GoToTest = 'testing.editFocusedTest', + HideTestAction = 'testing.hideTest', + OpenOutputPeek = 'testing.openOutputPeek', + RefreshTestsAction = 'testing.refreshTests', + ReRunFailedTests = 'testing.reRunFailTests', + ReRunLastRun = 'testing.reRunLastRun', + RunAction = 'testing.run', + RunAllAction = 'testing.runAll', + RunAtCursor = 'testing.runAtCursor', + RunCurrentFile = 'testing.runCurrentFile', + RunSelectedAction = 'testing.runSelected', + RunUsingProfileAction = 'testing.runUsing', + SearchForTestExtension = 'testing.searchForTestExtension', + SelectDefaultTestProfiles = 'testing.selectDefaultTestProfiles', + ShowMostRecentOutputAction = 'testing.showMostRecentOutput', + TestingSortByDurationAction = 'testing.sortByDuration', + TestingSortByLocationAction = 'testing.sortByLocation', + TestingSortByStatusAction = 'testing.sortByStatus', + TestingViewAsListAction = 'testing.viewAsList', + TestingViewAsTreeAction = 'testing.viewAsTree', + ToggleAutoRun = 'testing.toggleautoRun', + ToggleInlineTestOutput = 'testing.toggleInlineTestOutput', + UnhideTestAction = 'testing.unhideTest', +} diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index a4ba03fa7f2..30308a2a8fb 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -14,7 +14,7 @@ import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtensio import { IColorRegistry, Extensions as ColorRegistryExtensions } from 'vs/platform/theme/common/colorRegistry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Color } from 'vs/base/common/color'; -import { ColorScheme } from 'vs/platform/theme/common/theme'; +import { ColorScheme, isHighContrast } from 'vs/platform/theme/common/theme'; import { colorThemeSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { isCancellationError, onUnexpectedError } from 'vs/base/common/errors'; import { IQuickInputButton, IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; @@ -376,7 +376,7 @@ registerAction2(class extends Action2 { const picks: QuickPickInput[] = [ ...toEntries(themes.filter(t => t.type === ColorScheme.LIGHT), localize('themes.category.light', "light themes")), ...toEntries(themes.filter(t => t.type === ColorScheme.DARK), localize('themes.category.dark', "dark themes")), - ...toEntries(themes.filter(t => t.type === ColorScheme.HIGH_CONTRAST), localize('themes.category.hc', "high contrast themes")), + ...toEntries(themes.filter(t => isHighContrast(t.type)), localize('themes.category.hc', "high contrast themes")), ]; await picker.openQuickPick(picks, currentTheme); } diff --git a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts index 6199e5a05fa..adf11e1f50e 100644 --- a/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts +++ b/src/vs/workbench/contrib/timeline/browser/timeline.contribution.ts @@ -14,14 +14,13 @@ import { TimelineHasProviderContext, TimelineService } from 'vs/workbench/contri import { TimelinePane } from './timelinePane'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { ISubmenuItem, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ICommandHandler, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ExplorerFolderContext } from 'vs/workbench/contrib/files/common/files'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - const timelineViewIcon = registerIcon('timeline-view-icon', Codicon.history, localize('timelineViewIcon', 'View icon of the timeline view.')); const timelineOpenIcon = registerIcon('timeline-open', Codicon.history, localize('timelineOpenIcon', 'Icon for the open timeline action.')); @@ -98,4 +97,14 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, ({ when: ContextKeyExpr.and(ExplorerFolderContext.toNegated(), ResourceContextKey.HasResource, TimelineHasProviderContext) })); +const timelineFilter = registerIcon('timeline-filter', Codicon.filter, localize('timelineFilter', 'Icon for the filter timeline action.')); + +MenuRegistry.appendMenuItem(MenuId.TimelineTitle, { + submenu: MenuId.TimelineFilterSubMenu, + title: localize('filterTimeline', "Filter Timeline"), + group: 'navigation', + order: 100, + icon: timelineFilter +}); + registerSingleton(ITimelineService, TimelineService, true); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 73bd9a41fe3..accdad217fe 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -15,7 +15,7 @@ import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { Iterable } from 'vs/base/common/iterator'; import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { basename } from 'vs/base/common/path'; +import { ILabelService } from 'vs/platform/label/common/label'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; @@ -50,6 +50,7 @@ import { isString } from 'vs/base/common/types'; import { renderMarkdownAsPlaintext } from 'vs/base/browser/markdownRenderer'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; const ItemHeight = 22; @@ -187,7 +188,7 @@ class LoadMoreCommand { readonly handle = 'vscode-command:loadMore'; readonly timestamp = 0; readonly description = undefined; - readonly detail = undefined; + readonly tooltip = undefined; readonly contextValue = undefined; // Make things easier for duck typing readonly id = undefined; @@ -257,6 +258,8 @@ export class TimelinePane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @ILabelService private readonly labelService: ILabelService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -265,8 +268,8 @@ export class TimelinePane extends ViewPane { this.followActiveEditorContext = TimelineFollowActiveEditorContext.bindTo(this.contextKeyService); this.excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); - configurationService.onDidChangeConfiguration(this.onConfigurationChanged, this); + this._register(configurationService.onDidChangeConfiguration(this.onConfigurationChanged, this)); this._register(timelineService.onDidChangeProviders(this.onProvidersChanged, this)); this._register(timelineService.onDidChangeTimeline(this.onTimelineChanged, this)); this._register(timelineService.onDidChangeUri(uri => this.setUri(uri), this)); @@ -323,7 +326,7 @@ export class TimelinePane extends ViewPane { } this.uri = uri; - this.updateFilename(uri ? basename(uri.fsPath) : undefined); + this.updateFilename(uri ? this.labelService.getUriBasenameLabel(uri) : undefined); this.treeRenderer?.setUri(uri); this.loadTimeline(true); } @@ -353,7 +356,7 @@ export class TimelinePane extends ViewPane { const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - if ((uri?.toString(true) === this.uri?.toString(true) && uri !== undefined) || + if ((this.uriIdentityService.extUri.isEqual(uri, this.uri) && uri !== undefined) || // Fallback to match on fsPath if we are dealing with files or git schemes (uri?.fsPath === this.uri?.fsPath && (uri?.scheme === Schemas.file || uri?.scheme === 'git') && (this.uri?.scheme === Schemas.file || this.uri?.scheme === 'git'))) { @@ -396,7 +399,7 @@ export class TimelinePane extends ViewPane { } private onTimelineChanged(e: TimelineChangeEvent) { - if (e?.uri === undefined || e.uri.toString(true) !== this.uri?.toString(true)) { + if (e?.uri === undefined || this.uriIdentityService.extUri.isEqual(e.uri, this.uri)) { const timeline = this.timelinesBySource.get(e.id); if (timeline === undefined) { return; @@ -786,11 +789,11 @@ export class TimelinePane extends ViewPane { if (this.pendingRequests.size !== 0) { this.setLoadingUriMessage(); } else { - this.updateFilename(basename(this.uri.fsPath)); + this.updateFilename(this.labelService.getUriBasenameLabel(this.uri)); this.message = localize('timeline.noTimelineInfo', "No timeline information was provided."); } } else { - this.updateFilename(basename(this.uri.fsPath)); + this.updateFilename(this.labelService.getUriBasenameLabel(this.uri)); this.message = undefined; } @@ -894,9 +897,9 @@ export class TimelinePane extends ViewPane { } }, keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), - multipleSelectionSupport: true, + multipleSelectionSupport: false, overrideStyles: { - listBackground: this.getBackgroundColor(), + listBackground: this.getBackgroundColor() } }); @@ -966,7 +969,7 @@ export class TimelinePane extends ViewPane { } setLoadingUriMessage() { - const file = this.uri && basename(this.uri.fsPath); + const file = this.uri && this.labelService.getUriBasenameLabel(this.uri); this.updateFilename(file); this.message = file ? localize('timeline.loading', "Loading timeline for {0}...", file) : ''; } @@ -1156,14 +1159,14 @@ class TimelineTreeRenderer implements ITreeRenderer('timeline.excludeSources') ?? []); - for (const source of this.timelineService.getSources()) { this.sourceDisposables.add(registerAction2(class extends Action2 { constructor() { super({ id: `timeline.toggleExcludeSource:${source.id}`, - title: { value: localize('timeline.filterSource', "Include: {0}", source.label), original: `Include: ${source.label}` }, - category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + title: source.label, menu: { - id: MenuId.TimelineTitle, - group: '2_sources', + id: MenuId.TimelineFilterSubMenu, + group: 'navigation', }, toggled: ContextKeyExpr.regex(`config.timeline.excludeSources`, new RegExp(`\\b${escapeRegExpCharacters(source.id)}\\b`)).negate() }); diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index 32e18e7a91f..edf2a850a02 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -21,18 +21,32 @@ export function toKey(extension: ExtensionIdentifier | string, source: string) { export const TimelinePaneId = 'timeline'; export interface TimelineItem { + + /** + * The handle of the item must be unique across all the + * timeline items provided by this source. + */ handle: string; + + /** + * The identifier of the timeline provider this timeline item is from. + */ source: string; id?: string; - timestamp: number; + label: string; + description?: string; + tooltip?: string | IMarkdownString | undefined; + + timestamp: number; + accessibilityInformation?: IAccessibilityInformation; + icon?: URI; iconDark?: URI; themeIcon?: ThemeIcon; - description?: string; - detail?: string | IMarkdownString | undefined; + command?: Command; contextValue?: string; @@ -41,8 +55,21 @@ export interface TimelineItem { } export interface TimelineChangeEvent { + + /** + * The identifier of the timeline provider this event is from. + */ id: string; + + /** + * The resource that has timeline entries changed or `undefined` + * if not known. + */ uri: URI | undefined; + + /** + * Whether to drop all timeline entries and refresh them again. + */ reset: boolean; } @@ -57,7 +84,12 @@ export interface InternalTimelineOptions { } export interface Timeline { + + /** + * The identifier of the timeline provider this timeline is from. + */ source: string; + items: TimelineItem[]; paging?: { @@ -77,8 +109,20 @@ export interface TimelineSource { } export interface TimelineProviderDescriptor { + + /** + * An identifier of the source of the timeline items. This can be used to filter sources. + */ id: string; + + /** + * A human-readable string describing the source of the timeline items. This can be used as the display label when filtering sources. + */ label: string; + + /** + * The resource scheme(s) this timeline provider is providing entries for. + */ scheme: string | string[]; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 1e6dde4eb70..52bc586f90d 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -6,7 +6,6 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -// import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions, TimelinePaneId } from './timeline'; @@ -39,132 +38,6 @@ export class TimelineService implements ITimelineService { ) { this.hasProviderContext = TimelineHasProviderContext.bindTo(this.contextKeyService); this.updateHasProviderContext(); - - // let source = 'fast-source'; - // this.registerTimelineProvider({ - // scheme: '*', - // id: source, - // label: 'Fast Source', - // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { - // if (options.cursor === undefined) { - // return Promise.resolve({ - // source: source, - // items: [ - // { - // handle: `${source}|1`, - // id: '1', - // label: 'Fast Timeline1', - // description: '', - // timestamp: Date.now(), - // source: source - // }, - // { - // handle: `${source}|2`, - // id: '2', - // label: 'Fast Timeline2', - // description: '', - // timestamp: Date.now() - 3000000000, - // source: source - // } - // ], - // paging: { - // cursor: 'next' - // } - // }); - // } - // return Promise.resolve({ - // source: source, - // items: [ - // { - // handle: `${source}|3`, - // id: '3', - // label: 'Fast Timeline3', - // description: '', - // timestamp: Date.now() - 4000000000, - // source: source - // }, - // { - // handle: `${source}|4`, - // id: '4', - // label: 'Fast Timeline4', - // description: '', - // timestamp: Date.now() - 300000000000, - // source: source - // } - // ], - // paging: { - // cursor: undefined - // } - // }); - // }, - // dispose() { } - // }); - - // let source = 'slow-source'; - // this.registerTimelineProvider({ - // scheme: '*', - // id: source, - // label: 'Slow Source', - // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { - // return new Promise(resolve => setTimeout(() => { - // resolve({ - // source: source, - // items: [ - // { - // handle: `${source}|1`, - // id: '1', - // label: 'Slow Timeline1', - // description: basename(uri.fsPath), - // timestamp: Date.now(), - // source: source - // }, - // { - // handle: `${source}|2`, - // id: '2', - // label: 'Slow Timeline2', - // description: basename(uri.fsPath), - // timestamp: new Date(0).getTime(), - // source: source - // } - // ] - // }); - // }, 5000)); - // }, - // dispose() { } - // }); - - // source = 'very-slow-source'; - // this.registerTimelineProvider({ - // scheme: '*', - // id: source, - // label: 'Very Slow Source', - // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { - // return new Promise(resolve => setTimeout(() => { - // resolve({ - // source: source, - // items: [ - // { - // handle: `${source}|1`, - // id: '1', - // label: 'VERY Slow Timeline1', - // description: basename(uri.fsPath), - // timestamp: Date.now(), - // source: source - // }, - // { - // handle: `${source}|2`, - // id: '2', - // label: 'VERY Slow Timeline2', - // description: basename(uri.fsPath), - // timestamp: new Date(0).getTime(), - // source: source - // } - // ] - // }); - // }, 10000)); - // }, - // dispose() { } - // }); } getSources() { @@ -172,7 +45,7 @@ export class TimelineService implements ITimelineService { } getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { - this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); + this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString()}`); const provider = this.providers.get(id); if (provider === undefined) { diff --git a/src/vs/workbench/contrib/watermark/browser/media/watermark.css b/src/vs/workbench/contrib/watermark/browser/media/watermark.css index 909e2b692e0..2c45ebd4b16 100644 --- a/src/vs/workbench/contrib/watermark/browser/media/watermark.css +++ b/src/vs/workbench/contrib/watermark/browser/media/watermark.css @@ -65,10 +65,12 @@ color: rgba(255,255,255,.6); } -.monaco-workbench.hc-black .part.editor > .content.empty > .watermark dt { +.monaco-workbench.hc-black .part.editor > .content.empty > .watermark dt, +.monaco-workbench.hc-light .part.editor > .content.empty > .watermark dt { color: var(--vscode-editor-foreground); } -.monaco-workbench.hc-black .part.editor > .content.empty > .watermark dl { +.monaco-workbench.hc-black .part.editor > .content.empty > .watermark dl, +.monaco-workbench.hc-light .part.editor > .content.empty > .watermark dl { color: var(--vscode-editor-foreground); opacity: 1; } diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index d7f8e8d49ca..799c6820bba 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -27,7 +27,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { assertIsDefined } from 'vs/base/common/types'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; -import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileCommands'; +import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachKeybindingLabelStyler } from 'vs/platform/theme/common/styler'; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index c757d8e482d..48beccb020a 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -22,7 +22,6 @@ const searchParams = new URL(location.toString()).searchParams; const ID = searchParams.get('id'); const onElectron = searchParams.get('platform') === 'electron'; const expectedWorkerVersion = parseInt(searchParams.get('swVersion')); -const parentOrigin = searchParams.get('parentOrigin'); /** * Use polling to track focus of main webview and iframes within the webview @@ -309,8 +308,43 @@ const hostMessaging = new class HostMessaging { handlers.push(handler); } - signalReady() { - window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]); + async signalReady() { + const start = (/** @type {string} */ parentOrigin) => { + window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]); + }; + + const parentOrigin = searchParams.get('parentOrigin'); + const id = searchParams.get('id'); + + const hostname = location.hostname; + + if (!crypto.subtle) { + // cannot validate, not running in a secure context + throw new Error(`Cannot validate in current context!`); + } + + // Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined + // compute a sha-256 composed of `parentOrigin` and `salt` converted to base 32 + let parentOriginHash; + try { + const strData = JSON.stringify({ parentOrigin, salt: id }); + const encoder = new TextEncoder(); + const arrData = encoder.encode(strData); + const hash = await crypto.subtle.digest('sha-256', arrData); + const hashArray = Array.from(new Uint8Array(hash)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 + parentOriginHash = BigInt(`0x${hashHex}`).toString(32).padStart(52, '0'); + } catch (err) { + throw err instanceof Error ? err : new Error(String(err)); + } + + if (hostname === parentOriginHash || hostname.startsWith(parentOriginHash + '.')) { + // validation succeeded! + return start(parentOrigin); + } + + throw new Error(`Expected '${parentOriginHash}' as hostname or subdomain!`); } }(); @@ -389,6 +423,12 @@ const initData = { /** @type {string | undefined} */ themeName: undefined, + + /** @type {boolean} */ + screenReader: false, + + /** @type {boolean} */ + reduceMotion: false, }; hostMessaging.onMessage('did-load-resource', (_event, data) => { @@ -421,11 +461,19 @@ const applyStyles = (document, body) => { } if (body) { - body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast'); + body.classList.remove('vscode-light', 'vscode-dark', 'vscode-high-contrast', 'vscode-reduce-motion', 'vscode-using-screen-reader'); if (initData.activeTheme) { body.classList.add(initData.activeTheme); } + if (initData.reduceMotion) { + body.classList.add('vscode-reduce-motion'); + } + + if (initData.screenReader) { + body.classList.add('vscode-using-screen-reader'); + } + body.dataset.vscodeThemeKind = initData.activeTheme; body.dataset.vscodeThemeName = initData.themeName || ''; } @@ -742,6 +790,8 @@ onDomReady(() => { initData.styles = data.styles; initData.activeTheme = data.activeTheme; initData.themeName = data.themeName; + initData.reduceMotion = data.reduceMotion; + initData.screenReader = data.screenReader; const target = getActiveFrame(); if (!target) { @@ -878,7 +928,7 @@ onDomReady(() => { } const contentDocument = assertIsDefined(newFrame.contentDocument); - if (contentDocument.location.pathname === '/fake.html' && contentDocument.readyState !== 'loading') { + if (contentDocument.location.pathname.endsWith('/fake.html') && contentDocument.readyState !== 'loading') { clearInterval(interval); onFrameLoaded(contentDocument); } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 10166588de3..efb577a072b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -16,6 +16,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { localize } from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -30,6 +31,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; +import { parentOriginHash } from 'vs/workbench/browser/webview'; import { asWebviewUri, decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/common/webview'; import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; @@ -99,7 +101,10 @@ namespace WebviewState { export class WebviewElement extends Disposable implements IWebview, WebviewFindDelegate { public readonly id: string; - protected readonly iframeId: string; + + private readonly iframeId: string; + private readonly encodedWebviewOriginPromise: Promise; + private encodedWebviewOrigin: string | undefined; protected get platform(): string { return 'browser'; } @@ -144,6 +149,8 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD protected readonly _webviewFindWidget: WebviewFindWidget | undefined; public readonly checkImeCompletionState = true; + private _disposed = false; + constructor( id: string, private readonly options: WebviewOptions, @@ -161,11 +168,13 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD @ITelemetryService private readonly _telemetryService: ITelemetryService, @ITunnelService private readonly _tunnelService: ITunnelService, @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, ) { super(); this.id = id; this.iframeId = generateUuid(); + this.encodedWebviewOriginPromise = parentOriginHash(window.origin, this.iframeId).then(id => this.encodedWebviewOrigin = id); this.content = { html: '', @@ -183,11 +192,11 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD const subscription = this._register(addDisposableListener(window, 'message', (e: MessageEvent) => { - if (e?.data?.target !== this.iframeId) { + if (!this.encodedWebviewOrigin || e?.data?.target !== this.iframeId) { return; } - if (e.origin !== this.webviewContentOrigin) { + if (e.origin !== this.webviewContentOrigin(this.encodedWebviewOrigin)) { console.log(`Skipped renderer receiving message due to mismatched origins: ${e.origin} ${this.webviewContentOrigin}`); return; } @@ -328,16 +337,8 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._register(Event.runAndSubscribe(webviewThemeDataProvider.onThemeDataChanged, () => this.style())); - /* __GDPR__ - "webview.createWebview" : { - "extension": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "webviewElementType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this._telemetryService.publicLog('webview.createWebview', { - extension: extension?.id.value, - webviewElementType: 'iframe', - }); + this._register(_accessibilityService.onDidChangeReducedMotion(() => this.style())); + this._register(_accessibilityService.onDidChangeScreenReaderOptimized(() => this.style())); this._confirmBeforeClose = configurationService.getValue('window.confirmBeforeClose'); @@ -353,10 +354,16 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.styledFindWidget(); } - this.initElement(extension, options); + this.encodedWebviewOriginPromise.then(encodedWebviewOrigin => { + if (!this._disposed) { + this.initElement(encodedWebviewOrigin, extension, options); + } + }); } override dispose(): void { + this._disposed = true; + this.element?.remove(); this._element = undefined; @@ -436,7 +443,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD return element; } - private initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) { + private initElement(encodedWebviewOrigin: string, extension: WebviewExtensionDescription | undefined, options: WebviewOptions) { // The extensionId and purpose in the URL are used for filtering in js-debug: const params: { [key: string]: string } = { id: this.iframeId, @@ -460,7 +467,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1754872 const fileName = isFirefox ? 'index-no-csp.html' : 'index.html'; - this.element!.setAttribute('src', `${this.webviewContentEndpoint}/${fileName}?${queryString}`); + this.element!.setAttribute('src', `${this.webviewContentEndpoint(encodedWebviewOrigin)}/${fileName}?${queryString}`); } public mountTo(parent: HTMLElement) { @@ -474,22 +481,17 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD parent.appendChild(this.element); } - protected get webviewContentEndpoint(): string { - const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id); + protected webviewContentEndpoint(encodedWebviewOrigin: string): string { + const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin); if (endpoint[endpoint.length - 1] === '/') { return endpoint.slice(0, endpoint.length - 1); } return endpoint; } - private _webviewContentOrigin?: string; - - private get webviewContentOrigin(): string { - if (!this._webviewContentOrigin) { - const uri = URI.parse(this.webviewContentEndpoint); - this._webviewContentOrigin = uri.scheme + '://' + uri.authority.toLowerCase(); - } - return this._webviewContentOrigin; + private webviewContentOrigin(encodedWebviewOrigin: string): string { + const uri = URI.parse(this.webviewContentEndpoint(encodedWebviewOrigin)); + return uri.scheme + '://' + uri.authority.toLowerCase(); } private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): void { @@ -523,16 +525,15 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._onMissingCsp.fire(this.extension.id); } - type TelemetryClassification = { - extension?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; - }; - type TelemetryData = { - extension?: string; + const payload = { + extension: this.extension.id.value + } as const; + + type Classification = { + extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; owner: 'mjbvz'; comment: 'The id of the extension that created the webview.' }; }; - this._telemetryService.publicLog2('webviewMissingCsp', { - extension: this.extension.id.value - }); + this._telemetryService.publicLog2('webviewMissingCsp', payload); } } @@ -636,7 +637,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD styles = this.options.transformCssVariables(styles); } - this._send('styles', { styles, activeTheme, themeName: themeLabel }); + const reduceMotion = this._accessibilityService.isMotionReduced(); + const screenReader = this._accessibilityService.isScreenReaderOptimized(); + + this._send('styles', { styles, activeTheme, themeName: themeLabel, reduceMotion, screenReader }); this.styledFindWidget(); } diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts index 3a756e0c8bc..5d77ddbc1d9 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts @@ -8,6 +8,7 @@ import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { consumeStream } from 'vs/base/common/stream'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IMenuService } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -60,11 +61,12 @@ export class ElectronWebviewElement extends WebviewElement { @IMainProcessService mainProcessService: IMainProcessService, @INotificationService notificationService: INotificationService, @INativeHostService private readonly nativeHostService: INativeHostService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, ) { super(id, options, contentOptions, extension, webviewThemeDataProvider, configurationService, contextMenuService, menuService, notificationService, environmentService, - fileService, logService, remoteAuthorityResolverService, telemetryService, tunnelService, instantiationService); + fileService, logService, remoteAuthorityResolverService, telemetryService, tunnelService, instantiationService, accessibilityService); this._webviewKeyboardHandler = new WindowIgnoreMenuShortcutsManager(configurationService, mainProcessService, nativeHostService); @@ -92,8 +94,8 @@ export class ElectronWebviewElement extends WebviewElement { } } - protected override get webviewContentEndpoint(): string { - return `${Schemas.vscodeWebview}://${this.iframeId}`; + protected override webviewContentEndpoint(iframeId: string): string { + return `${Schemas.vscodeWebview}://${iframeId}`; } protected override streamToBuffer(stream: VSBufferReadableStream): Promise { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts index bdeb6dc9aac..7ffb3adf213 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewIconManager.ts @@ -66,7 +66,7 @@ export class WebviewIconManager implements IDisposable { const webviewSelector = `.show-file-icons .webview-${key}-name-file-icon::before`; try { cssRules.push( - `.monaco-workbench.vs ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.light)}; }`, + `.monaco-workbench.vs ${webviewSelector}, .monaco-workbench.hc-light ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.light)}; }`, `.monaco-workbench.vs-dark ${webviewSelector}, .monaco-workbench.hc-black ${webviewSelector} { content: ""; background-image: ${dom.asCSSUrl(value.dark)}; }` ); } catch { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index b381d279bd9..f7beac7c5bb 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { generateUuid } from 'vs/base/common/uuid'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -140,7 +142,6 @@ export class WebviewViewPane extends ViewPane { return; } - this.layoutWebview(); } @@ -160,7 +161,7 @@ export class WebviewViewPane extends ViewPane { this._activated = true; - const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); + const webviewId = generateUuid(); const webview = this.webviewService.createWebviewOverlay( webviewId, { purpose: WebviewContentPurpose.WebviewView }, @@ -170,9 +171,6 @@ export class WebviewViewPane extends ViewPane { webview.state = this.viewState[storageKeys.webviewState]; this._webview.value = webview; - webview.state = this.viewState[storageKeys.webviewState]; - this._webview.value = webview; - if (this._container) { this._webview.value?.layoutWebviewOverElement(this._container); } @@ -185,6 +183,15 @@ export class WebviewViewPane extends ViewPane { this.viewState[storageKeys.webviewState] = webview.state; })); + // Re-dispatch all drag events back to the drop target to support view drag drop + for (const event of [EventType.DRAG, EventType.DRAG_END, EventType.DRAG_ENTER, EventType.DRAG_LEAVE, EventType.DRAG_START]) { + this._webviewDisposables.add(addDisposableListener(this._webview.value.container!, event, e => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.dropTargetElement.dispatchEvent(new DragEvent(e.type, e)); + })); + } + this._webviewDisposables.add(new WebviewWindowDragMonitor(() => this._webview.value)); const source = this._webviewDisposables.add(new CancellationTokenSource()); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index 3a8fa8c1d9a..7e276d7a2a7 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -24,12 +24,12 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorResolution } from 'vs/platform/editor/common/editor'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from 'vs/base/common/platform'; import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { StartupPageContribution, } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; export * as icons from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedIcons'; @@ -182,73 +182,51 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor) { - const commandService = accessor.get(ICommandService); - const contextService = accessor.get(IContextKeyService); - const quickInputService = accessor.get(IQuickInputService); - const gettingStartedService = accessor.get(IWalkthroughsService); + private getQuickPickItems( + contextService: IContextKeyService, + gettingStartedService: IWalkthroughsService + ): IQuickPickItem[] { const categories = gettingStartedService.getWalkthroughs(); - const selection = await quickInputService.pick(categories + return categories .filter(c => contextService.contextMatchesRules(c.when)) .map(x => ({ id: x.id, label: x.title, detail: x.description, description: x.source, - })), { canPickMany: false, matchOnDescription: true, matchOnDetail: true, title: localize('pickWalkthroughs', "Open Walkthrough...") }); - if (selection) { - commandService.executeCommand('workbench.action.openWalkthrough', selection.id); - } + })); + } + + async run(accessor: ServicesAccessor) { + const commandService = accessor.get(ICommandService); + const contextService = accessor.get(IContextKeyService); + const quickInputService = accessor.get(IQuickInputService); + const gettingStartedService = accessor.get(IWalkthroughsService); + const extensionService = accessor.get(IExtensionService); + + const quickPick = quickInputService.createQuickPick(); + quickPick.canSelectMany = false; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + quickPick.title = localize('pickWalkthroughs', "Open Walkthrough..."); + quickPick.items = this.getQuickPickItems(contextService, gettingStartedService); + quickPick.busy = true; + quickPick.onDidAccept(() => { + const selection = quickPick.selectedItems[0]; + if (selection) { + commandService.executeCommand('workbench.action.openWalkthrough', selection.id); + } + quickPick.hide(); + }); + quickPick.onDidHide(() => quickPick.dispose()); + quickPick.show(); + await extensionService.whenInstalledExtensionsRegistered(); + quickPick.busy = false; + await gettingStartedService.installedExtensionsRegistered; + quickPick.items = this.getQuickPickItems(contextService, gettingStartedService); } }); -const prefersReducedMotionConfig = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.welcomePage.preferReducedMotion': { - scope: ConfigurationScope.APPLICATION, - type: 'boolean', - default: true, - description: localize('workbench.welcomePage.preferReducedMotion', "When enabled, reduce motion in welcome page.") - } - } -} as const; - -const prefersStandardMotionConfig = { - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.welcomePage.preferReducedMotion': { - scope: ConfigurationScope.APPLICATION, - type: 'boolean', - default: false, - description: localize('workbench.welcomePage.preferReducedMotion', "When enabled, reduce motion in welcome page.") - } - } -} as const; - -class WorkbenchConfigurationContribution { - constructor( - @IInstantiationService _instantiationService: IInstantiationService, - @IConfigurationService _configurationService: IConfigurationService, - @IWorkbenchAssignmentService _experimentSevice: IWorkbenchAssignmentService, - ) { - this.registerConfigs(_experimentSevice); - } - - private async registerConfigs(_experimentSevice: IWorkbenchAssignmentService) { - const preferReduced = await _experimentSevice.getTreatment('welcomePage.preferReducedMotion').catch(e => false); - if (preferReduced) { - configurationRegistry.updateConfigurations({ add: [prefersReducedMotionConfig], remove: [prefersStandardMotionConfig] }); - } - else { - configurationRegistry.updateConfigurations({ add: [prefersStandardMotionConfig], remove: [prefersReducedMotionConfig] }); - } - } -} - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(WorkbenchConfigurationContribution, LifecyclePhase.Restored); - export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | 'webworker' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote or serverless contexts may be different from the platform of the UI")); class WorkspacePlatformContribution { constructor( @@ -308,6 +286,31 @@ configurationRegistry.registerConfiguration({ tags: ['experimental'], default: 'off', description: localize('workbench.welcomePage.videoTutorials', "When enabled, the get started page has additional links to video tutorials.") + }, + 'workbench.startupEditor': { + 'scope': ConfigurationScope.RESOURCE, + 'type': 'string', + 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'], + 'enumDescriptions': [ + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page, with content to aid in getting started with VS Code and extensions."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise. Note: This is only observed as a global configuration, it will be ignored if set in a workspace or folder configuration."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), + ], + 'default': 'welcomePage', + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") + }, + 'workbench.welcomePage.preferReducedMotion': { + scope: ConfigurationScope.APPLICATION, + type: 'boolean', + default: false, + deprecationMessage: localize('deprecationMessage', "Deprecated, use the global `workbench.reduceMotion`."), + description: localize('workbench.welcomePage.preferReducedMotion', "When enabled, reduce motion in welcome page.") } } }); + + +Registry.as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(StartupPageContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index ca94b5f2166..c009d712229 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -15,7 +15,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { hiddenEntriesConfigurationKey, IResolvedWalkthrough, IResolvedWalkthroughStep, IWalkthroughsService } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { welcomePageBackground, welcomePageProgressBackground, welcomePageProgressForeground, welcomePageTileBackground, welcomePageTileHoverBackground, welcomePageTileShadow } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors'; -import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, simpleCheckboxBackground, simpleCheckboxBorder, simpleCheckboxForeground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, descriptionForeground, focusBorder, foreground, checkboxBackground, checkboxBorder, checkboxForeground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { firstSessionDateStorageKey, ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -34,7 +34,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IWindowOpenable } from 'vs/platform/window/common/window'; import { splitName } from 'vs/base/common/labels'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { isMacintosh, locale } from 'vs/base/common/platform'; +import { isMacintosh } from 'vs/base/common/platform'; import { Delayer, Throttler } from 'vs/base/common/async'; import { GettingStartedInput } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedInput'; import { GroupDirection, GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -45,18 +45,12 @@ import { attachButtonStyler } from 'vs/platform/theme/common/styler'; import { Link } from 'vs/platform/opener/browser/link'; import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; -import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { generateUuid } from 'vs/base/common/uuid'; -import { TokenizationRegistry } from 'vs/editor/common/languages'; -import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; -import { ResourceMap } from 'vs/base/common/map'; import { IFileService } from 'vs/platform/files/common/files'; import { parse } from 'vs/base/common/marshalling'; -import { joinPath } from 'vs/base/common/resources'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { asWebviewUri } from 'vs/workbench/common/webview'; import { Schemas } from 'vs/base/common/network'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { coalesce, equals, flatten } from 'vs/base/common/arrays'; @@ -71,9 +65,11 @@ import { getTelemetryLevel } from 'vs/platform/telemetry/common/telemetryUtils'; import { WorkbenchStateContext } from 'vs/workbench/common/contextkeys'; import { OpenFolderViaWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; import { OpenRecentAction } from 'vs/workbench/browser/actions/windowActions'; -import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; -import { restoreWalkthroughsConfigurationKey, RestoreWalkthroughsConfigurationValue } from 'vs/workbench/contrib/welcomePage/browser/welcomePage'; +import { restoreWalkthroughsConfigurationKey, RestoreWalkthroughsConfigurationValue } from 'vs/workbench/contrib/welcomeGettingStarted/browser/startupPage'; +import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -103,8 +99,8 @@ const parsedStartEntries: IWelcomePageStartEntry[] = startEntries.map((e, i) => })); type GettingStartedActionClassification = { - command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; - argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + command: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'Help understand what actions are most commonly taken on the getting started page' }; + argument: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'JacksonKearl'; comment: 'As above' }; }; type GettingStartedActionEvent = { @@ -153,6 +149,8 @@ export class GettingStartedPage extends EditorPane { private layoutMarkdown: (() => void) | undefined; + private detailsRenderer: GettingStartedDetailsRenderer; + private webviewID = generateUuid(); private categoriesSlideDisposables: DisposableStore; @@ -179,6 +177,7 @@ export class GettingStartedPage extends EditorPane { @IHostService private readonly hostService: IHostService, @IWebviewService private readonly webviewService: IWebviewService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, ) { super(GettingStartedPage.ID, telemetryService, themeService, storageService); @@ -194,6 +193,8 @@ export class GettingStartedPage extends EditorPane { this.categoriesSlideDisposables = this._register(new DisposableStore()); + this.detailsRenderer = new GettingStartedDetailsRenderer(this.fileService, this.notificationService, this.extensionService, this.languageService); + this.contextService = this._register(contextService.createScoped(this.container)); inWelcomeContext.bindTo(this.contextService).set(true); embedderIdentifierContext.bindTo(this.contextService).set(productService.embedderIdentifier); @@ -250,7 +251,6 @@ export class GettingStartedPage extends EditorPane { this.container.classList.toggle('animatable', this.shouldAnimate()); } })); - ourStep.done = step.done; if (category.id === this.currentWalkthrough?.id) { @@ -274,8 +274,15 @@ export class GettingStartedPage extends EditorPane { this.recentlyOpened = workspacesService.getRecentlyOpened(); } + // remove when 'workbench.welcomePage.preferReducedMotion' deprecated private shouldAnimate() { - return !this.configurationService.getValue(REDUCED_MOTION_KEY); + if (this.configurationService.getValue(REDUCED_MOTION_KEY)) { + return false; + } + if (this.accessibilityService.isMotionReduced()) { + return false; + } + return true; } private getWalkthroughCompletionStats(walkthrough: IResolvedWalkthrough): { stepsComplete: number; stepsTotal: number } { @@ -457,71 +464,6 @@ export class GettingStartedPage extends EditorPane { } } - private svgCache = new ResourceMap>(); - private readAndCacheSVGFile(path: URI): Promise { - if (!this.svgCache.has(path)) { - this.svgCache.set(path, (async () => { - try { - const bytes = await this.fileService.readFile(path); - return bytes.value.toString(); - } catch (e) { - this.notificationService.error('Error reading svg document at `' + path + '`: ' + e); - return ''; - } - })()); - } - return assertIsDefined(this.svgCache.get(path)); - } - - private mdCache = new ResourceMap>(); - private async readAndCacheStepMarkdown(path: URI): Promise { - if (!this.mdCache.has(path)) { - this.mdCache.set(path, (async () => { - try { - const moduleId = JSON.parse(path.query).moduleId; - if (moduleId) { - return new Promise(resolve => { - require([moduleId], content => { - const markdown = content.default(); - resolve(renderMarkdownDocument(markdown, this.extensionService, this.languageService, true, true)); - }); - }); - } - } catch { } - try { - const localizedPath = path.with({ path: path.path.replace(/\.md$/, `.nls.${locale}.md`) }); - - const generalizedLocale = locale?.replace(/-.*$/, ''); - const generalizedLocalizedPath = path.with({ path: path.path.replace(/\.md$/, `.nls.${generalizedLocale}.md`) }); - - const fileExists = (file: URI) => this.fileService - .stat(file) - .then((stat) => !!stat.size) // Double check the file actually has content for fileSystemProviders that fake `stat`. #131809 - .catch(() => false); - - const [localizedFileExists, generalizedLocalizedFileExists] = await Promise.all([ - fileExists(localizedPath), - fileExists(generalizedLocalizedPath), - ]); - - const bytes = await this.fileService.readFile( - localizedFileExists - ? localizedPath - : generalizedLocalizedFileExists - ? generalizedLocalizedPath - : path); - - const markdown = bytes.value.toString(); - return renderMarkdownDocument(markdown, this.extensionService, this.languageService, true, true); - } catch (e) { - this.notificationService.error('Error reading markdown document at `' + path + '`: ' + e); - return ''; - } - })()); - } - return assertIsDefined(this.mdCache.get(path)); - } - private getHiddenCategories(): Set { return new Set(JSON.parse(this.storageService.get(hiddenEntriesConfigurationKey, StorageScope.GLOBAL, '[]'))); } @@ -586,14 +528,14 @@ export class GettingStartedPage extends EditorPane { const webview = this.stepDisposables.add(this.webviewService.createWebviewElement(this.webviewID, {}, {}, undefined)); webview.mountTo(this.stepMediaComponent); - webview.html = await this.renderSVG(media.path); + webview.html = await this.detailsRenderer.renderSVG(media.path); let isDisposed = false; this.stepDisposables.add(toDisposable(() => { isDisposed = true; })); this.stepDisposables.add(this.themeService.onDidColorThemeChange(async () => { // Render again since color vars change - const body = await this.renderSVG(media.path); + const body = await this.detailsRenderer.renderSVG(media.path); if (!isDisposed) { // Make sure we weren't disposed of in the meantime webview.html = body; } @@ -627,7 +569,7 @@ export class GettingStartedPage extends EditorPane { const webview = this.stepDisposables.add(this.webviewService.createWebviewElement(this.webviewID, {}, { localResourceRoots: [media.root], allowScripts: true }, undefined)); webview.mountTo(this.stepMediaComponent); - const rawHTML = await this.renderMarkdown(media.path, media.base); + const rawHTML = await this.detailsRenderer.renderMarkdown(media.path, media.base); webview.html = rawHTML; const serializedContextKeyExprs = rawHTML.match(/checked-on=\"([^'][^"]*)\"/g)?.map(attr => attr.slice('checked-on="'.length, -1) @@ -663,7 +605,7 @@ export class GettingStartedPage extends EditorPane { this.stepDisposables.add(this.themeService.onDidColorThemeChange(async () => { // Render again since syntax highlighting of code blocks may have changed - const body = await this.renderMarkdown(media.path, media.base); + const body = await this.detailsRenderer.renderMarkdown(media.path, media.base); if (!isDisposed) { // Make sure we weren't disposed of in the meantime webview.html = body; postTrueKeysMessage(); @@ -717,7 +659,7 @@ export class GettingStartedPage extends EditorPane { node.setAttribute('aria-expanded', 'false'); } }); - setTimeout(() => (stepElement as HTMLElement).focus(), delayFocus ? SLIDE_TRANSITION_TIME_MS : 0); + setTimeout(() => (stepElement as HTMLElement).focus(), delayFocus && this.shouldAnimate() ? SLIDE_TRANSITION_TIME_MS : 0); this.editorInput.selectedStep = id; @@ -733,174 +675,12 @@ export class GettingStartedPage extends EditorPane { this.detailsScrollbar?.scanDomNode(); } - private updateMediaSourceForColorMode(element: HTMLImageElement, sources: { hc: URI; dark: URI; light: URI }) { + private updateMediaSourceForColorMode(element: HTMLImageElement, sources: { hcDark: URI; hcLight: URI; dark: URI; light: URI }) { const themeType = this.themeService.getColorTheme().type; const src = sources[themeType].toString(true).replace(/ /g, '%20'); element.srcset = src.toLowerCase().endsWith('.svg') ? src : (src + ' 1.5x'); } - private async renderSVG(path: URI): Promise { - const content = await this.readAndCacheSVGFile(path); - const nonce = generateUuid(); - const colorMap = TokenizationRegistry.getColorMap(); - - const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; - return ` - - - - - - - - ${content} - - `; - } - - private async renderMarkdown(path: URI, base: URI): Promise { - const content = await this.readAndCacheStepMarkdown(path); - const nonce = generateUuid(); - const colorMap = TokenizationRegistry.getColorMap(); - - const uriTranformedContent = content.replace(/src="([^"]*)"/g, (_, src: string) => { - if (src.startsWith('https://')) { return `src="${src}"`; } - - const path = joinPath(base, src); - const transformed = asWebviewUri(path).toString(); - return `src="${transformed}"`; - }); - - const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; - - const inDev = document.location.protocol === 'http:'; - const imgSrcCsp = inDev ? 'img-src https: data: http:' : 'img-src https: data:'; - - return ` - - - - - - - - - ${uriTranformedContent} - - - - `; - } - createEditor(parent: HTMLElement) { if (this.detailsPageScrollbar) { this.detailsPageScrollbar.dispose(); } if (this.categoriesPageScrollbar) { this.categoriesPageScrollbar.dispose(); } @@ -929,7 +709,7 @@ export class GettingStartedPage extends EditorPane { private async buildCategoriesSlide() { this.categoriesSlideDisposables.clear(); - const showOnStartupCheckbox = new Checkbox({ + const showOnStartupCheckbox = new Toggle({ icon: Codicon.check, actionClassName: 'getting-started-checkbox', isChecked: this.configurationService.getValue(configurationKey) === 'welcomePage', @@ -1095,7 +875,11 @@ export class GettingStartedPage extends EditorPane { title: localize('recent', "Recent"), klass: 'recently-opened', limit: 5, - empty: $('.empty-recent', {}, 'You have no recent folders,', $('button.button-link', { 'x-dispatch': 'openFolder' }, 'open a folder'), 'to start.'), + empty: $('.empty-recent', {}, + localize('noRecents', "You have no recent folders,"), + $('button.button-link', { 'x-dispatch': 'openFolder' }, localize('openFolder', "open a folder")), + localize('toStart', "to start.")), + more: $('.more', {}, $('button.button-link', { @@ -1755,18 +1539,18 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-category .featured { border-top-color: ${newBadgeBackground}; }`); } - const checkboxBackground = theme.getColor(simpleCheckboxBackground); - if (checkboxBackground) { - collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { background-color: ${checkboxBackground} !important; }`); + const checkboxBackgroundColor = theme.getColor(checkboxBackground); + if (checkboxBackgroundColor) { + collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { background-color: ${checkboxBackgroundColor} !important; }`); } - const checkboxForeground = theme.getColor(simpleCheckboxForeground); - if (checkboxForeground) { - collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { color: ${checkboxForeground} !important; }`); + const checkboxForegroundColor = theme.getColor(checkboxForeground); + if (checkboxForegroundColor) { + collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { color: ${checkboxForegroundColor} !important; }`); } - const checkboxBorder = theme.getColor(simpleCheckboxBorder); - if (checkboxBorder) { - collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { border-color: ${checkboxBorder} !important; }`); + const checkboxBorderColor = theme.getColor(checkboxBorder); + if (checkboxBorderColor) { + collector.addRule(`.monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlide .getting-started-checkbox { border-color: ${checkboxBorderColor} !important; }`); } }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts index 35e241edce8..90af3e2eee4 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedColors.ts @@ -7,11 +7,11 @@ import { darken, inputBackground, editorWidgetBackground, lighten, registerColor import { localize } from 'vs/nls'; // Seprate from main module to break dependency cycles between welcomePage and gettingStarted. -export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hc: null }, localize('welcomePage.background', 'Background color for the Welcome page.')); +export const welcomePageBackground = registerColor('welcomePage.background', { light: null, dark: null, hcDark: null, hcLight: null }, localize('welcomePage.background', 'Background color for the Welcome page.')); -export const welcomePageTileBackground = registerColor('welcomePage.tileBackground', { dark: editorWidgetBackground, light: editorWidgetBackground, hc: '#000' }, localize('welcomePage.tileBackground', 'Background color for the tiles on the Get Started page.')); -export const welcomePageTileHoverBackground = registerColor('welcomePage.tileHoverBackground', { dark: lighten(editorWidgetBackground, .2), light: darken(editorWidgetBackground, .1), hc: null }, localize('welcomePage.tileHoverBackground', 'Hover background color for the tiles on the Get Started.')); -export const welcomePageTileShadow = registerColor('welcomePage.tileShadow', { light: widgetShadow, dark: widgetShadow, hc: null }, localize('welcomePage.tileShadow', 'Shadow color for the Welcome page walkthrough category buttons.')); +export const welcomePageTileBackground = registerColor('welcomePage.tileBackground', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: '#000', hcLight: editorWidgetBackground }, localize('welcomePage.tileBackground', 'Background color for the tiles on the Get Started page.')); +export const welcomePageTileHoverBackground = registerColor('welcomePage.tileHoverBackground', { dark: lighten(editorWidgetBackground, .2), light: darken(editorWidgetBackground, .1), hcDark: null, hcLight: null }, localize('welcomePage.tileHoverBackground', 'Hover background color for the tiles on the Get Started.')); +export const welcomePageTileShadow = registerColor('welcomePage.tileShadow', { light: widgetShadow, dark: widgetShadow, hcDark: null, hcLight: null }, localize('welcomePage.tileShadow', 'Shadow color for the Welcome page walkthrough category buttons.')); -export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', { light: inputBackground, dark: inputBackground, hc: inputBackground }, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); -export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', { light: textLinkForeground, dark: textLinkForeground, hc: textLinkForeground }, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); +export const welcomePageProgressBackground = registerColor('welcomePage.progress.background', { light: inputBackground, dark: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('welcomePage.progress.background', 'Foreground color for the Welcome page progress bars.')); +export const welcomePageProgressForeground = registerColor('welcomePage.progress.foreground', { light: textLinkForeground, dark: textLinkForeground, hcDark: textLinkForeground, hcLight: textLinkForeground }, localize('welcomePage.progress.foreground', 'Background color for the Welcome page progress bars.')); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts new file mode 100644 index 00000000000..b4d6e89f34a --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer.ts @@ -0,0 +1,260 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { generateUuid } from 'vs/base/common/uuid'; +import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; +import { TokenizationRegistry } from 'vs/editor/common/languages'; +import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from 'vs/workbench/contrib/markdown/browser/markdownDocumentRenderer'; +import { URI } from 'vs/base/common/uri'; +import { locale } from 'vs/base/common/platform'; +import { joinPath } from 'vs/base/common/resources'; +import { assertIsDefined } from 'vs/base/common/types'; +import { asWebviewUri } from 'vs/workbench/common/webview'; +import { ResourceMap } from 'vs/base/common/map'; +import { IFileService } from 'vs/platform/files/common/files'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + + +export class GettingStartedDetailsRenderer { + private mdCache = new ResourceMap>(); + private svgCache = new ResourceMap>(); + + constructor( + @IFileService private readonly fileService: IFileService, + @INotificationService private readonly notificationService: INotificationService, + @IExtensionService private readonly extensionService: IExtensionService, + @ILanguageService private readonly languageService: ILanguageService, + ) { } + + async renderMarkdown(path: URI, base: URI): Promise { + const content = await this.readAndCacheStepMarkdown(path, base); + const nonce = generateUuid(); + const colorMap = TokenizationRegistry.getColorMap(); + + const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; + + const inDev = document.location.protocol === 'http:'; + const imgSrcCsp = inDev ? 'img-src https: data: http:' : 'img-src https: data:'; + + return ` + + + + + + + + + ${content} + + + + `; + } + + async renderSVG(path: URI): Promise { + const content = await this.readAndCacheSVGFile(path); + const nonce = generateUuid(); + const colorMap = TokenizationRegistry.getColorMap(); + + const css = colorMap ? generateTokensCSSForColorMap(colorMap) : ''; + return ` + + + + + + + + ${content} + + `; + } + + private readAndCacheSVGFile(path: URI): Promise { + if (!this.svgCache.has(path)) { + this.svgCache.set(path, this.readContentsOfPath(path, false)); + } + return assertIsDefined(this.svgCache.get(path)); + } + + private readAndCacheStepMarkdown(path: URI, base: URI): Promise { + if (!this.mdCache.has(path)) { + this.mdCache.set(path, + this.readContentsOfPath(path).then(rawContents => + renderMarkdownDocument(transformUris(rawContents, base), this.extensionService, this.languageService, true, true))); + } + return assertIsDefined(this.mdCache.get(path)); + } + + private async readContentsOfPath(path: URI, useModuleId = true): Promise { + try { + const moduleId = JSON.parse(path.query).moduleId; + if (useModuleId && moduleId) { + const contents = await new Promise(c => { + require([moduleId], content => { + c(content.default()); + }); + }); + return contents; + } + } catch { } + + try { + const localizedPath = path.with({ path: path.path.replace(/\.md$/, `.nls.${locale}.md`) }); + + const generalizedLocale = locale?.replace(/-.*$/, ''); + const generalizedLocalizedPath = path.with({ path: path.path.replace(/\.md$/, `.nls.${generalizedLocale}.md`) }); + + const fileExists = (file: URI) => this.fileService + .stat(file) + .then((stat) => !!stat.size) // Double check the file actually has content for fileSystemProviders that fake `stat`. #131809 + .catch(() => false); + + const [localizedFileExists, generalizedLocalizedFileExists] = await Promise.all([ + fileExists(localizedPath), + fileExists(generalizedLocalizedPath), + ]); + + const bytes = await this.fileService.readFile( + localizedFileExists + ? localizedPath + : generalizedLocalizedFileExists + ? generalizedLocalizedPath + : path); + + return bytes.value.toString(); + } catch (e) { + this.notificationService.error('Error reading markdown document at `' + path + '`: ' + e); + return ''; + } + } +} + +const transformUri = (src: string, base: URI) => { + const path = joinPath(base, src); + return asWebviewUri(path).toString(); +}; + +const transformUris = (content: string, base: URI): string => content + .replace(/src="([^"]*)"/g, (_, src: string) => { + if (src.startsWith('https://')) { return `src="${src}"`; } + return `src="${transformUri(src, base)}"`; + }) + .replace(/!\[([^\]]*)\]\(([^)]*)\)/g, (_, title: string, src: string) => { + if (src.startsWith('https://')) { return `![${title}](${src})`; } + return `![${title}](${transformUri(src, base)})`; + }); diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint.ts index cd36dedd298..47c767d1249 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedExtensionPoint.ts @@ -90,7 +90,7 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo }, { type: 'object', - required: ['dark', 'light', 'hc'], + required: ['dark', 'light', 'hc', 'hcLight'], properties: { dark: { description: localize('walkthroughs.steps.media.image.path.dark.string', "Path to the image for dark themes, relative to extension directory."), @@ -103,6 +103,10 @@ export const walkthroughsExtensionPoint = ExtensionsRegistry.registerExtensionPo hc: { description: localize('walkthroughs.steps.media.image.path.hc.string', "Path to the image for hc themes, relative to extension directory."), type: 'string', + }, + hcLight: { + description: localize('walkthroughs.steps.media.image.path.hcLight.string', "Path to the image for hc light themes, relative to extension directory."), + type: 'string', } } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index a761a041a58..75b3891db8d 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -79,7 +79,7 @@ export interface IWalkthroughStep { order: number; completionEvents: string[]; media: - | { type: 'image'; path: { hc: URI; light: URI; dark: URI }; altText: string } + | { type: 'image'; path: { hcDark: URI; hcLight: URI; light: URI; dark: URI }; altText: string } | { type: 'svg'; path: URI; altText: string } | { type: 'markdown'; path: URI; base: URI; root: URI }; } @@ -281,17 +281,18 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ ? URI.parse(path, true) : FileAccess.asFileUri(joinPath(extension.extensionLocation, path)); - const convertExtensionRelativePathsToBrowserURIs = (path: string | { hc: string; dark: string; light: string }): { hc: URI; dark: URI; light: URI } => { + const convertExtensionRelativePathsToBrowserURIs = (path: string | { hc: string; hcLight?: string; dark: string; light: string }): { hcDark: URI; hcLight: URI; dark: URI; light: URI } => { const convertPath = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asBrowserUri(joinPath(extension.extensionLocation, path)); if (typeof path === 'string') { const converted = convertPath(path); - return { hc: converted, dark: converted, light: converted }; + return { hcDark: converted, hcLight: converted, dark: converted, light: converted }; } else { return { - hc: convertPath(path.hc), + hcDark: convertPath(path.hc), + hcLight: convertPath(path.hcLight ?? path.light), light: convertPath(path.light), dark: convertPath(path.dark) }; @@ -427,7 +428,11 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ if (sectionToOpen && this.configurationService.getValue('workbench.welcomePage.walkthroughs.openOnInstall')) { type GettingStartedAutoOpenClassification = { - id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + id: { + classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; + owner: 'JacksonKearl'; + comment: 'Used to understand what walkthroughs are consulted most frequently'; + }; }; type GettingStartedAutoOpenEvent = { id: string; @@ -660,20 +665,21 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ const parseDescription = (desc: string): LinkedText[] => desc.split('\n').filter(x => x).map(text => parseLinkedText(text)); -const convertInternalMediaPathToFileURI = (path: string) => path.startsWith('https://') +export const convertInternalMediaPathToFileURI = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/' + path, require); const convertInternalMediaPathToBrowserURI = (path: string) => path.startsWith('https://') ? URI.parse(path, true) : FileAccess.asBrowserUri('vs/workbench/contrib/welcomeGettingStarted/common/media/' + path, require); -const convertInternalMediaPathsToBrowserURIs = (path: string | { hc: string; dark: string; light: string }): { hc: URI; dark: URI; light: URI } => { +const convertInternalMediaPathsToBrowserURIs = (path: string | { hc: string; hcLight?: string; dark: string; light: string }): { hcDark: URI; hcLight: URI; dark: URI; light: URI } => { if (typeof path === 'string') { const converted = convertInternalMediaPathToBrowserURI(path); - return { hc: converted, dark: converted, light: converted }; + return { hcDark: converted, hcLight: converted, dark: converted, light: converted }; } else { return { - hc: convertInternalMediaPathToBrowserURI(path.hc), + hcDark: convertInternalMediaPathToBrowserURI(path.hc), + hcLight: convertInternalMediaPathToBrowserURI(path.hcLight ?? path.light), light: convertInternalMediaPathToBrowserURI(path.light), dark: convertInternalMediaPathToBrowserURI(path.dark) }; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css index e56244feadd..eb269e524fe 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/media/gettingStarted.css @@ -60,7 +60,8 @@ display: block; } -.monaco-workbench.hc-black .part.editor>.content .gettingStartedContainer .subtitle { +.monaco-workbench.hc-black .part.editor>.content .gettingStartedContainer .subtitle, +.monaco-workbench.hc-light .part.editor>.content .gettingStartedContainer .subtitle { font-weight: 200; } @@ -91,11 +92,18 @@ padding: 12px 24px; } +/* duplicated until the getting-started specific setting is removed */ .monaco-workbench .part.editor>.content .gettingStartedContainer.animatable .gettingStartedSlide { /* keep consistant with SLIDE_TRANSITION_TIME_MS in gettingStarted.ts */ transition: left 0.25s, opacity 0.25s; } +.monaco-workbench.reduce-motion .part.editor>.content .gettingStartedContainer .gettingStartedSlide, +.monaco-workbench.reduce-motion .part.editor>.content .gettingStartedContainer.animatable .gettingStartedSlide { + /* keep consistant with SLIDE_TRANSITION_TIME_MS in gettingStarted.ts */ + transition: left 0.0s, opacity 0.0s; +} + .monaco-workbench .part.editor>.content .gettingStartedContainer .gettingStartedSlideCategories>.gettingStartedCategoriesContainer { display: grid; height: 100%; diff --git a/src/vs/workbench/contrib/welcomePage/browser/welcomePage.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts similarity index 95% rename from src/vs/workbench/contrib/welcomePage/browser/welcomePage.ts rename to src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts index 3d7a08e1b30..e7a2c51207e 100644 --- a/src/vs/workbench/contrib/welcomePage/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/startupPage.ts @@ -32,7 +32,7 @@ const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; const telemetryOptOutStorageKey = 'workbench.telemetryOptOutShown'; -export class WelcomePageContribution implements IWorkbenchContribution { +export class StartupPageContribution implements IWorkbenchContribution { constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -62,7 +62,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { && !this.storageService.get(telemetryOptOutStorageKey, StorageScope.GLOBAL) ) { this.storageService.store(telemetryOptOutStorageKey, true, StorageScope.GLOBAL, StorageTarget.USER); - await this.openWelcome(true); + await this.openGettingStarted(true); return; } @@ -70,7 +70,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { return; } - const enabled = isWelcomePageEnabled(this.configurationService, this.contextService, this.environmentService); + const enabled = isStartupPageEnabled(this.configurationService, this.contextService, this.environmentService); if (enabled && this.lifecycleService.startupKind !== StartupKind.ReloadedWindow) { const hasBackups = await this.workingCopyBackupService.hasBackups(); if (hasBackups) { return; } @@ -87,7 +87,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { if (openWithReadme) { await this.openReadme(); } else { - await this.openWelcome(); + await this.openGettingStarted(); } } } @@ -134,12 +134,12 @@ export class WelcomePageContribution implements IWorkbenchContribution { this.editorService.openEditors(readmes.filter(readme => !isMarkDown(readme)).map(readme => ({ resource: readme }))), ]); } else { - await this.openWelcome(); + await this.openGettingStarted(); } } } - private async openWelcome(showTelemetryNotice?: boolean) { + private async openGettingStarted(showTelemetryNotice?: boolean) { const startupEditorTypeID = gettingStartedInputTypeId; const editor = this.editorService.activeEditor; @@ -155,7 +155,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { } } -function isWelcomePageEnabled(configurationService: IConfigurationService, contextService: IWorkspaceContextService, environmentService: IWorkbenchEnvironmentService) { +function isStartupPageEnabled(configurationService: IConfigurationService, contextService: IWorkspaceContextService, environmentService: IWorkbenchEnvironmentService) { if (environmentService.skipWelcome) { return false; } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 37e3959aab2..a091be87b19 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/workbench/contrib/welcomeGettingStarted/common/media/example_markdown_media'; +import 'vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker'; import 'vs/workbench/contrib/welcomeGettingStarted/common/media/notebookProfile'; import { localize } from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; @@ -24,7 +24,7 @@ export type BuiltinGettingStartedStep = { completionEvents?: string[]; when?: string; media: - | { type: 'image'; path: string | { hc: string; light: string; dark: string }; altText: string } + | { type: 'image'; path: string | { hc: string; hcLight?: string; light: string; dark: string }; altText: string } | { type: 'svg'; path: string; altText: string } | { type: 'markdown'; path: string }; }; @@ -116,7 +116,7 @@ export const startEntries: GettingStartedStartEntryContent = [ when: '!openFolderWorkspaceSupport && workbenchState == \'workspace\'', content: { type: 'startEntry', - command: 'command:workbench.action.setRootFolder', + command: 'command:workbench.action.files.openFolderViaWorkspace', } }, { @@ -198,7 +198,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ 'onSettingChanged:workbench.colorTheme', 'onCommand:workbench.action.selectTheme' ], - media: { type: 'markdown', path: 'example_markdown_media', } + media: { type: 'markdown', path: 'theme_picker', } }, { id: 'settingsSync', @@ -284,7 +284,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ 'onSettingChanged:workbench.colorTheme', 'onCommand:workbench.action.selectTheme' ], - media: { type: 'markdown', path: 'example_markdown_media', } + media: { type: 'markdown', path: 'theme_picker', } }, { id: 'settingsSyncWeb', diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/dark.png b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/dark.png index bac2bc8d2d8..67ad6fcbd35 100644 Binary files a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/dark.png and b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/dark.png differ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light-hc.png b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light-hc.png new file mode 100644 index 00000000000..983550cd30a Binary files /dev/null and b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light-hc.png differ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light.png b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light.png index 81aa74dc4de..14a472108e5 100644 Binary files a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light.png and b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/light.png differ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/monokai.png b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/monokai.png index ea0926178e0..68c4aff78b1 100644 Binary files a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/monokai.png and b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/monokai.png differ diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/example_markdown_media.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker.ts similarity index 79% rename from src/vs/workbench/contrib/welcomeGettingStarted/common/media/example_markdown_media.ts rename to src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker.ts index ddc6e9b4a95..c86c8f4f807 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/media/example_markdown_media.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker.ts @@ -18,7 +18,11 @@ export default () => ` - ${escape(localize('HighContrast', "High Contrast"))} + ${escape(localize('HighContrast', "Dark High Contrast"))} + + + + ${escape(localize('HighContrastLight', "Light High Contrast"))} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts new file mode 100644 index 00000000000..e8339183da5 --- /dev/null +++ b/src/vs/workbench/contrib/welcomeGettingStarted/test/browser/gettingStartedMarkdownRenderer.test.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { FileAccess } from 'vs/base/common/network'; +import { LanguageService } from 'vs/editor/common/services/languageService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer'; +import { convertInternalMediaPathToFileURI } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService'; +import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices'; + + +suite('Getting Started Markdown Renderer', () => { + test('renders theme picker markdown with images', async () => { + const fileService = new TestFileService(); + const languageService = new LanguageService(); + const renderer = new GettingStartedDetailsRenderer(fileService, new TestNotificationService(), new TestExtensionService(), languageService); + const mdPath = convertInternalMediaPathToFileURI('theme_picker').with({ query: JSON.stringify({ moduleId: 'vs/workbench/contrib/welcomeGettingStarted/common/media/theme_picker' }) }); + const mdBase = FileAccess.asFileUri('vs/workbench/contrib/welcomeGettingStarted/common/media/', require); + const rendered = await renderer.renderMarkdown(mdPath, mdBase); + const imageSrcs = [...rendered.matchAll(/img src="[^"]*"/g)].map(match => match[0]); + for (const src of imageSrcs) { + const targetSrcFormat = /^img src="https:\/\/file%2B.vscode-resource.vscode-webview.net\/.*\/vs\/workbench\/contrib\/welcomeGettingStarted\/common\/media\/.*.png"$/; + assert(targetSrcFormat.test(src), `${src} didnt match regex`); + } + languageService.dispose(); + }); +}); diff --git a/src/vs/workbench/contrib/welcomeOverlay/browser/media/welcomeOverlay.css b/src/vs/workbench/contrib/welcomeOverlay/browser/media/welcomeOverlay.css index 32a3bb90fc2..88de8da3412 100644 --- a/src/vs/workbench/contrib/welcomeOverlay/browser/media/welcomeOverlay.css +++ b/src/vs/workbench/contrib/welcomeOverlay/browser/media/welcomeOverlay.css @@ -28,7 +28,10 @@ .monaco-workbench.hc-black.blur-background #workbench\.parts\.panel, .monaco-workbench.hc-black.blur-background #workbench\.parts\.sidebar, -.monaco-workbench.hc-black.blur-background #workbench\.parts\.editor { +.monaco-workbench.hc-black.blur-background #workbench\.parts\.editor, +.monaco-workbench.hc-light.blur-background #workbench\.parts\.panel, +.monaco-workbench.hc-light.blur-background #workbench\.parts\.sidebar, +.monaco-workbench.hc-light.blur-background #workbench\.parts\.editor { opacity: .2; } diff --git a/src/vs/workbench/contrib/welcomePage/browser/welcomePage.contribution.ts b/src/vs/workbench/contrib/welcomePage/browser/welcomePage.contribution.ts deleted file mode 100644 index 41dc79b8bb1..00000000000 --- a/src/vs/workbench/contrib/welcomePage/browser/welcomePage.contribution.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { WelcomePageContribution, } from 'vs/workbench/contrib/welcomePage/browser/welcomePage'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; - -Registry.as(ConfigurationExtensions.Configuration) - .registerConfiguration({ - ...workbenchConfigurationNodeBase, - 'properties': { - 'workbench.startupEditor': { - 'scope': ConfigurationScope.RESOURCE, - 'type': 'string', - 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench'], - 'enumDescriptions': [ - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page, with content to aid in getting started with VS Code and extensions."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise. Note: This is only observed as a global configuration, it will be ignored if set in a workspace or folder configuration."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled file (only applies when opening an empty window)."), - localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), - ], - 'default': 'welcomePage', - 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") - }, - } - }); - -Registry.as(WorkbenchExtensions.Workbench) - .registerWorkbenchContribution(WelcomePageContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css index 0bdf1be5cdc..77383d51f14 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/media/walkThroughPart.css @@ -135,7 +135,8 @@ display: initial; } -.monaco-workbench.hc-black .part.editor > .content .walkThroughContent .monaco-editor { +.monaco-workbench.hc-black .part.editor > .content .walkThroughContent .monaco-editor, +.monaco-workbench.hc-light .part.editor > .content .walkThroughContent .monaco-editor { border-width: 1px; border-style: solid; } diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index 554e5a11150..fc8d43ad75b 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -490,10 +490,10 @@ export class WalkThroughPart extends EditorPane { // theming -export const embeddedEditorBackground = registerColor('walkThrough.embeddedEditorBackground', { dark: null, light: null, hc: null }, localize('walkThrough.embeddedEditorBackground', 'Background color for the embedded editors on the Interactive Playground.')); +export const embeddedEditorBackground = registerColor('walkThrough.embeddedEditorBackground', { dark: null, light: null, hcDark: null, hcLight: null }, localize('walkThrough.embeddedEditorBackground', 'Background color for the embedded editors on the Interactive Playground.')); registerThemingParticipant((theme, collector) => { - const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); + const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hcDark: null, hcLight: null }); if (color) { collector.addRule(`.monaco-workbench .part.editor > .content .walkThroughContent .monaco-editor-background, .monaco-workbench .part.editor > .content .walkThroughContent .margin-view-overlays { background: ${color}; }`); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 7aca0b78959..714d83dc166 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -29,8 +29,6 @@ import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions } from 'vs/ import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceContextService, IWorkspaceFoldersWillChangeEvent, toWorkspaceIdentifier, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { isWeb } from 'vs/base/common/platform'; -import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { dirname, resolve } from 'vs/base/common/path'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; @@ -630,7 +628,7 @@ registerAction2(class extends Action2 { super({ id: CONFIGURE_TRUST_COMMAND_ID, title: { original: 'Configure Workspace Trust', value: localize('configureWorkspaceTrust', "Configure Workspace Trust") }, - precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), + precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), category: localize('workspacesCategory', "Workspaces"), f1: true }); @@ -648,14 +646,14 @@ registerAction2(class extends Action2 { super({ id: MANAGE_TRUST_COMMAND_ID, title: { original: 'Manage Workspace Trust', value: localize('manageWorkspaceTrust', "Manage Workspace Trust") }, - precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), + precondition: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)), category: localize('workspacesCategory', "Workspaces"), f1: true, menu: { id: MenuId.GlobalActivity, group: '6_workspace_trust', order: 40, - when: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, IsWebContext.negate(), ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)) + when: ContextKeyExpr.and(WorkspaceTrustContext.IsEnabled, ContextKeyExpr.equals(`config.${WORKSPACE_TRUST_ENABLED}`, true)) }, }); } @@ -686,7 +684,6 @@ Registry.as(ConfigurationExtensions.Configuration) [WORKSPACE_TRUST_ENABLED]: { type: 'boolean', default: true, - included: !isWeb, description: localize('workspace.trust.description', "Controls whether or not workspace trust is enabled within VS Code."), tags: [WORKSPACE_TRUST_SETTING_TAG], scope: ConfigurationScope.APPLICATION, @@ -694,7 +691,6 @@ Registry.as(ConfigurationExtensions.Configuration) [WORKSPACE_TRUST_STARTUP_PROMPT]: { type: 'string', default: 'once', - included: !isWeb, description: localize('workspace.trust.startupPrompt.description', "Controls when the startup prompt to trust a workspace is shown."), tags: [WORKSPACE_TRUST_SETTING_TAG], scope: ConfigurationScope.APPLICATION, @@ -708,7 +704,6 @@ Registry.as(ConfigurationExtensions.Configuration) [WORKSPACE_TRUST_BANNER]: { type: 'string', default: 'untilDismissed', - included: !isWeb, description: localize('workspace.trust.banner.description', "Controls when the restricted mode banner is shown."), tags: [WORKSPACE_TRUST_SETTING_TAG], scope: ConfigurationScope.APPLICATION, @@ -722,7 +717,6 @@ Registry.as(ConfigurationExtensions.Configuration) [WORKSPACE_TRUST_UNTRUSTED_FILES]: { type: 'string', default: 'prompt', - included: !isWeb, markdownDescription: localize('workspace.trust.untrustedFiles.description', "Controls how to handle opening untrusted files in a trusted workspace. This setting also applies to opening files in an empty window which is trusted via `#{0}#`.", WORKSPACE_TRUST_EMPTY_WINDOW), tags: [WORKSPACE_TRUST_SETTING_TAG], scope: ConfigurationScope.APPLICATION, @@ -736,7 +730,6 @@ Registry.as(ConfigurationExtensions.Configuration) [WORKSPACE_TRUST_EMPTY_WINDOW]: { type: 'boolean', default: true, - included: !isWeb, markdownDescription: localize('workspace.trust.emptyWindow.description', "Controls whether or not the empty window is trusted by default within VS Code. When used with `#{0}#`, you can enable the full functionality of VS Code without prompting in an empty window.", WORKSPACE_TRUST_UNTRUSTED_FILES), tags: [WORKSPACE_TRUST_SETTING_TAG], scope: ConfigurationScope.APPLICATION diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index ad465887438..e4c63f35ce9 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -229,7 +229,7 @@ export class DesktopMain extends Disposable { this._register(RemoteFileSystemProviderClient.register(remoteAgentService, fileService, logService)); // User Data Provider - fileService.registerProvider(Schemas.userData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService))); + fileService.registerProvider(Schemas.vscodeUserData, this._register(new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.vscodeUserData, logService))); // URI Identity const uriIdentityService = new UriIdentityService(fileService); @@ -331,7 +331,7 @@ export class DesktopMain extends Disposable { } private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { - const configurationCache = new ConfigurationCache([Schemas.file, Schemas.userData] /* Cache all non native resources */, environmentService, fileService); + const configurationCache = new ConfigurationCache([Schemas.file, Schemas.vscodeUserData] /* Cache all non native resources */, environmentService, fileService); const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService); try { diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 5aa65d1fda8..23329c15f9e 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -197,6 +197,15 @@ export class NativeWindow extends Disposable { }] )); + ipcRenderer.on('vscode:showCredentialsError', (event: unknown, message: string) => this.notificationService.prompt( + Severity.Error, + localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", message), + [{ + label: localize('troubleshooting', "Troubleshooting Guide"), + run: () => this.openerService.open('https://go.microsoft.com/fwlink/?linkid=2190713') + }] + )); + // Fullscreen Events ipcRenderer.on('vscode:enterFullScreen', async () => setFullscreen(true)); ipcRenderer.on('vscode:leaveFullScreen', async () => setFullscreen(false)); @@ -319,7 +328,7 @@ export class NativeWindow extends Disposable { // Lifecycle this._register(this.lifecycleService.onBeforeShutdown(e => this.onBeforeShutdown(e))); this._register(this.lifecycleService.onBeforeShutdownError(e => this.onBeforeShutdownError(e))); - this._register(this.lifecycleService.onWillShutdown((e) => this.onWillShutdown(e))); + this._register(this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e))); } private onBeforeShutdown({ reason }: BeforeShutdownEvent): void { diff --git a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts index 4b6ca8c5039..af9a08af7c8 100644 --- a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts @@ -16,6 +16,7 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; interface AccessibilityMetrics { enabled: boolean; @@ -33,10 +34,11 @@ export class NativeAccessibilityService extends AccessibilityService implements @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, + @ILayoutService _layoutService: ILayoutService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @INativeHostService private readonly nativeHostService: INativeHostService ) { - super(contextKeyService, configurationService); + super(contextKeyService, _layoutService, configurationService); this.setAccessibilitySupport(environmentService.window.accessibilitySupport ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled); } diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 42505773acc..5f7431dafe9 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -36,11 +36,9 @@ interface IAccountUsage { lastUsed: number; } -const VSO_ALLOWED_EXTENSIONS = [ +const FIRST_PARTY_ALLOWED_EXTENSIONS = [ 'github.vscode-pull-request-github', - 'github.vscode-pull-request-github-insiders', 'vscode.git', - 'ms-vsonline.vsonline', 'github.remotehub', 'github.remotehub-insiders', 'github.codespaces', @@ -390,11 +388,13 @@ export class AuthenticationService extends Disposable implements IAuthentication } const remoteConnection = this.remoteAgentService.getConnection(); - const isVSO = remoteConnection !== null - ? remoteConnection.remoteAuthority.startsWith('vsonline') || remoteConnection.remoteAuthority.startsWith('codespaces') + // Right now, this is hardcoded to only happen in Codespaces and on web. + // TODO: this should be determined by the embedder so that this logic isn't in core. + const allowedAllowedExtensions = remoteConnection !== null + ? remoteConnection.remoteAuthority.startsWith('codespaces') : isWeb; - if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { + if (allowedAllowedExtensions && FIRST_PARTY_ALLOWED_EXTENSIONS.includes(extensionId)) { return true; } diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 7100b576b6f..2bd9b092c9a 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -233,7 +233,7 @@ class FileServiceBasedConfiguration extends Disposable { this.logService.trace(`Error while resolving configuration file '${resource.toString()}': ${errors.getErrorMessage(error)}`); if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND && (error).fileOperationResult !== FileOperationResult.FILE_NOT_DIRECTORY) { - errors.onUnexpectedError(error); + this.logService.error(error); } } return '{}'; @@ -307,7 +307,7 @@ class FileServiceBasedConfiguration extends Disposable { private handleFileOperationEvent(event: FileOperationEvent): boolean { // One of the resources has changed - if ((event.isOperation(FileOperation.CREATE) || event.isOperation(FileOperation.DELETE) || event.isOperation(FileOperation.WRITE)) + if ((event.isOperation(FileOperation.CREATE) || event.isOperation(FileOperation.COPY) || event.isOperation(FileOperation.DELETE) || event.isOperation(FileOperation.WRITE)) && this.allResources.some(resource => this.uriIdentityService.extUri.isEqual(event.resource, resource))) { return true; } @@ -504,7 +504,7 @@ class FileServiceBasedRemoteUserConfiguration extends Disposable { } private handleFileOperationEvent(event: FileOperationEvent): void { - if ((event.isOperation(FileOperation.CREATE) || event.isOperation(FileOperation.DELETE) || event.isOperation(FileOperation.WRITE)) + if ((event.isOperation(FileOperation.CREATE) || event.isOperation(FileOperation.COPY) || event.isOperation(FileOperation.DELETE) || event.isOperation(FileOperation.WRITE)) && this.uriIdentityService.extUri.isEqual(event.resource, this.configurationResource)) { this.reloadConfigurationScheduler.schedule(); } @@ -600,6 +600,7 @@ export class WorkspaceConfiguration extends Disposable { private readonly configurationCache: IConfigurationCache, private readonly fileService: IFileService, private readonly uriIdentityService: IUriIdentityService, + private readonly logService: ILogService, ) { super(); this.fileService = fileService; @@ -614,7 +615,7 @@ export class WorkspaceConfiguration extends Disposable { this._workspaceConfiguration = this._cachedConfiguration; this.waitAndInitialize(this._workspaceIdentifier); } else { - this.doInitialize(new FileServiceBasedWorkspaceConfiguration(this.fileService, this.uriIdentityService)); + this.doInitialize(new FileServiceBasedWorkspaceConfiguration(this.fileService, this.uriIdentityService, this.logService)); } } await this.reload(); @@ -663,7 +664,7 @@ export class WorkspaceConfiguration extends Disposable { private async waitAndInitialize(workspaceIdentifier: IWorkspaceIdentifier): Promise { await whenProviderRegistered(workspaceIdentifier.configPath, this.fileService); if (!(this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration)) { - const fileServiceBasedWorkspaceConfiguration = this._register(new FileServiceBasedWorkspaceConfiguration(this.fileService, this.uriIdentityService)); + const fileServiceBasedWorkspaceConfiguration = this._register(new FileServiceBasedWorkspaceConfiguration(this.fileService, this.uriIdentityService, this.logService)); await fileServiceBasedWorkspaceConfiguration.load(workspaceIdentifier, { scopes: WORKSPACE_SCOPES, skipRestricted: this.isUntrusted() }); this.doInitialize(fileServiceBasedWorkspaceConfiguration); this.onDidWorkspaceConfigurationChange(false, true); @@ -710,7 +711,8 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable { constructor( private readonly fileService: IFileService, - uriIdentityService: IUriIdentityService + uriIdentityService: IUriIdentityService, + private readonly logService: ILogService, ) { super(); @@ -719,7 +721,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable { this._register(Event.any( Event.filter(this.fileService.onDidFilesChange, e => !!this._workspaceIdentifier && e.contains(this._workspaceIdentifier.configPath)), - Event.filter(this.fileService.onDidRunOperation, e => !!this._workspaceIdentifier && (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.WRITE)) && uriIdentityService.extUri.isEqual(e.resource, this._workspaceIdentifier.configPath)) + Event.filter(this.fileService.onDidRunOperation, e => !!this._workspaceIdentifier && (e.isOperation(FileOperation.CREATE) || e.isOperation(FileOperation.COPY) || e.isOperation(FileOperation.DELETE) || e.isOperation(FileOperation.WRITE)) && uriIdentityService.extUri.isEqual(e.resource, this._workspaceIdentifier.configPath)) )(() => this.reloadConfigurationScheduler.schedule())); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); this.workspaceConfigWatcher = this._register(this.watchWorkspaceConfigurationFile()); @@ -747,7 +749,7 @@ class FileServiceBasedWorkspaceConfiguration extends Disposable { } catch (error) { const exists = await this.fileService.exists(this._workspaceIdentifier.configPath); if (exists) { - errors.onUnexpectedError(error); + this.logService.error(error); } } this.workspaceConfigurationModelParser.parse(contents, configurationParseOptions); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 0b39d04e024..45af17cc175 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -129,7 +129,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat this.initRemoteUserConfigurationBarrier.open(); } - this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, fileService, uriIdentityService)); + this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, fileService, uriIdentityService, logService)); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(fromCache => { this.onWorkspaceConfigurationChanged(fromCache).then(() => { this.workspace.initialized = this.workspaceConfiguration.initialized; diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts index 86b58fd04d2..7d7e3cb1267 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts @@ -94,7 +94,7 @@ suite('ConfigurationEditingService', () => { environmentService = TestEnvironmentService; instantiationService.stub(IEnvironmentService, environmentService); const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); - disposables.add(fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, logService)))); + disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 773453dc5d4..61cc2fc1b51 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -76,7 +76,7 @@ suite('WorkspaceContextService - Folder', () => { await fileService.createFolder(folder); const environmentService = TestEnvironmentService; - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -142,7 +142,7 @@ suite('WorkspaceContextService - Workspace', () => { const environmentService = TestEnvironmentService; const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); instantiationService.stub(IRemoteAgentService, remoteAgentService); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IWorkspaceContextService, testObject); @@ -200,7 +200,7 @@ suite('WorkspaceContextService - Workspace Editing', () => { const environmentService = TestEnvironmentService; const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IFileService, fileService); @@ -443,7 +443,7 @@ suite('WorkspaceService - Initialization', () => { environmentService = TestEnvironmentService; const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -692,7 +692,7 @@ suite('WorkspaceConfigurationService - Folder', () => { environmentService = TestEnvironmentService; const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -1363,7 +1363,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { environmentService = TestEnvironmentService; const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IFileService, fileService); @@ -2023,7 +2023,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { environmentService = TestEnvironmentService; const remoteEnvironmentPromise = new Promise>(c => resolveRemoteEnvironment = () => c({ settingsPath: remoteSettingsResource })); const remoteAgentService = instantiationService.stub(IRemoteAgentService, >{ getEnvironment: () => remoteEnvironmentPromise }); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); const configurationCache: IConfigurationCache = { read: () => Promise.resolve(''), write: () => Promise.resolve(), remove: () => Promise.resolve(), needsCaching: () => false }; testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); instantiationService.stub(IWorkspaceContextService, testObject); diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 473e7f2eca8..acbac82cf51 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -14,13 +14,12 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; -import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput'; import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; -import { ITextModel } from 'vs/editor/common/model'; export abstract class BaseConfigurationResolverService extends AbstractVariableResolverService { @@ -60,7 +59,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR getFilePath: (): string | undefined => { const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.userData, this.pathService.defaultUriScheme] + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] }); if (!fileResource) { return undefined; @@ -70,7 +69,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR getWorkspaceFolderPathForFile: (): string | undefined => { const fileResource = EditorResourceAccessor.getOriginalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY, - filterByScheme: [Schemas.file, Schemas.userData, this.pathService.defaultUriScheme] + filterByScheme: [Schemas.file, Schemas.vscodeUserData, this.pathService.defaultUriScheme] }); if (!fileResource) { return undefined; @@ -84,19 +83,18 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR getSelectedText: (): string | undefined => { const activeTextEditorControl = editorService.activeTextEditorControl; - let activeModel: ITextModel | null = null; + let activeControl: ICodeEditor | null = null; + if (isCodeEditor(activeTextEditorControl)) { - activeModel = activeTextEditorControl.getModel(); - } - if (isDiffEditor(activeTextEditorControl)) { - if (activeTextEditorControl.getOriginalEditor().hasTextFocus()) { - activeModel = activeTextEditorControl.getOriginalEditor().getModel(); - } else { - activeModel = activeTextEditorControl.getModifiedEditor().getModel(); - } + activeControl = activeTextEditorControl; + } else if (isDiffEditor(activeTextEditorControl)) { + const original = activeTextEditorControl.getOriginalEditor(); + const modified = activeTextEditorControl.getModifiedEditor(); + activeControl = original.hasWidgetFocus() ? original : modified; } - const activeSelection = activeTextEditorControl?.getSelection(); + const activeModel = activeControl?.getModel(); + const activeSelection = activeControl?.getSelection(); if (activeModel && activeSelection) { return activeModel.getValueInRange(activeSelection); } diff --git a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts index 3106a335007..a75a3c77ef7 100644 --- a/src/vs/workbench/services/configurationResolver/common/variableResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/variableResolver.ts @@ -28,7 +28,7 @@ export interface IVariableResolveContext { getLineNumber(): string | undefined; } -type Environment = { env?: IProcessEnvironment; userHome?: string }; +type Environment = { env: IProcessEnvironment | undefined; userHome: string | undefined }; export class AbstractVariableResolverService implements IConfigurationResolverService { @@ -67,7 +67,7 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe } public resolveWithEnvironment(environment: IProcessEnvironment, root: IWorkspaceFolder | undefined, value: string): string { - return this.recursiveResolve(this.prepareEnv(environment), root ? root.uri : undefined, value); + return this.recursiveResolve({ env: this.prepareEnv(environment), userHome: undefined }, root ? root.uri : undefined, value); } public async resolveAsync(root: IWorkspaceFolder | undefined, value: string): Promise; diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index e84dc0dd366..cbb7a21bcba 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -274,7 +274,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } private shouldUseSimplified(scheme: string): boolean { - return ![Schemas.file, Schemas.userData, Schemas.tmp].includes(scheme); + return ![Schemas.file, Schemas.vscodeUserData, Schemas.tmp].includes(scheme); } } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 89504a76e08..6071f168d6a 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -606,9 +606,17 @@ export class SimpleFileDialog { return UpdateResult.NotUpdated; } + private tryUpdateTrailing(value: URI) { + const ext = resources.extname(value); + if (ext) { + this.trailing = resources.basename(value); + } + } + private setActiveItems(value: string) { value = this.pathFromUri(this.tildaReplace(value)); - const inputBasename = resources.basename(this.remoteUriFrom(value)); + const asUri = this.remoteUriFrom(value); + const inputBasename = resources.basename(asUri); const userPath = this.constructFullUserPath(); // Make sure that the folder whose children we are currently viewing matches the path in the input const pathsEqual = equalsIgnoreCase(userPath, value.substring(0, userPath.length)) || @@ -627,11 +635,13 @@ export class SimpleFileDialog { this.userEnteredPathSegment = (userBasename === inputBasename) ? inputBasename : ''; this.autoCompletePathSegment = ''; this.filePickBox.activeItems = []; + this.tryUpdateTrailing(asUri); } } else { this.userEnteredPathSegment = inputBasename; this.autoCompletePathSegment = ''; this.filePickBox.activeItems = []; + this.tryUpdateTrailing(asUri); } } diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index ef2ac0fca5f..e4468a73200 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -63,7 +63,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil const setting = (this.configurationService.getValue('files.simpleDialog.enable') === true); const newWindowSetting = (this.configurationService.getValue('window.openFilesInNewWindow') === 'on'); return { - useSimplified: ((schema !== Schemas.file) && (schema !== Schemas.userData)) || setting, + useSimplified: ((schema !== Schemas.file) && (schema !== Schemas.vscodeUserData)) || setting, isSetting: newWindowSetting }; } diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index ca1bdcf6af9..9edd90ea440 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -216,7 +216,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver if (input) { this.sendEditorResolutionTelemetry(input.editor); if (input.editor.editorId !== selectedEditor.editorInfo.id) { - console.warn(`Editor ID Mismatch: ${input.editor.editorId} !== ${selectedEditor.editorInfo.id}. This will cause bugs. Please ensure editorInput.editorId matches the registered id`); + this.logService.warn(`Editor ID Mismatch: ${input.editor.editorId} !== ${selectedEditor.editorInfo.id}. This will cause bugs. Please ensure editorInput.editorId matches the registered id`); } return { ...input, group }; } @@ -746,7 +746,7 @@ export class EditorResolverService extends Disposable implements IEditorResolver private sendEditorResolutionTelemetry(chosenInput: EditorInput): void { type editorResolutionClassification = { - viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + viewType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; owner: 'lramos15'; comment: 'The id of the editor opened. Used to gain an undertsanding of what editors are most popular' }; }; type editorResolutionEvent = { viewType: string; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index d91974f937d..51015fde85a 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -14,7 +14,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, isEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, isEditorReplacement, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IUntypedEditorReplacement, IEditorService, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorsOptions, PreferredGroup, isPreferredGroup, IEditorsChangeEvent } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; @@ -697,6 +697,43 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion + //#region closeEditor() + + async closeEditor({ editor, groupId }: IEditorIdentifier, options?: ICloseEditorOptions): Promise { + const group = this.editorGroupService.getGroup(groupId); + + await group?.closeEditor(editor, options); + } + + //#endregion + + //#region closeEditors() + + async closeEditors(editors: IEditorIdentifier[], options?: ICloseEditorOptions): Promise { + const mapGroupToEditors = new Map(); + + for (const { editor, groupId } of editors) { + const group = this.editorGroupService.getGroup(groupId); + if (!group) { + continue; + } + + let editors = mapGroupToEditors.get(group); + if (!editors) { + editors = []; + mapGroupToEditors.set(group, editors); + } + + editors.push(editor); + } + + for (const [group, editors] of mapGroupToEditors) { + await group.closeEditors(editors, options); + } + } + + //#endregion + //#region findEditors() findEditors(resource: URI, options?: IFindEditorOptions): readonly IEditorIdentifier[]; @@ -709,13 +746,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { const typeId = URI.isUri(arg1) ? undefined : arg1.typeId; // Do a quick check for the resource via the editor observer - // which is a very efficient way to find an editor by resource - if (!this.editorsObserver.hasEditors(resource)) { - if (URI.isUri(arg1) || isUndefined(arg2)) { - return []; - } + // which is a very efficient way to find an editor by resource. + // However, we can only do that unless we are asked to find an + // editor on the secondary side of a side by side editor, because + // the editor observer provides fast lookups only for primary + // editors. + if (options?.supportSideBySide !== SideBySideEditor.ANY && options?.supportSideBySide !== SideBySideEditor.SECONDARY) { + if (!this.editorsObserver.hasEditors(resource)) { + if (URI.isUri(arg1) || isUndefined(arg2)) { + return []; + } - return undefined; + return undefined; + } } // Search only in specific group diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 016a0772389..d19dc93600c 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -579,7 +579,7 @@ export interface IEditorGroup { /** * Find out if the provided editor is pinned in the group. */ - isPinned(editor: EditorInput): boolean; + isPinned(editor: EditorInput | number): boolean; /** * Find out if the provided editor or index of editor is sticky in the group. diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 0abfba8e541..601636e9c48 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -9,7 +9,7 @@ import { IEditorPane, GroupIdentifier, IUntitledTextResourceEditorInput, IResour import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Event } from 'vs/base/common/event'; import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon'; -import { IEditorGroup, isEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ICloseEditorOptions, IEditorGroup, isEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { URI } from 'vs/base/common/uri'; import { IGroupModelChangeEvent } from 'vs/workbench/common/editor/editorGroupModel'; @@ -262,6 +262,16 @@ export interface IEditorService { */ isVisible(editor: EditorInput): boolean; + /** + * Close an editor in a specific editor group. + */ + closeEditor(editor: IEditorIdentifier, options?: ICloseEditorOptions): Promise; + + /** + * Close multiple editors in specific editor groups. + */ + closeEditors(editors: readonly IEditorIdentifier[], options?: ICloseEditorOptions): Promise; + /** * This method will return an entry for each editor that reports * a `resource` that matches the provided one in the group or diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index 516e62e4be9..8531394ea3f 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -1232,16 +1232,31 @@ suite('EditorGroupsService', () => { const group = part.activeGroup; assert.strictEqual(group.isEmpty, true); - const input1 = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.file('foo/bar1'), `${TEST_EDITOR_INPUT_ID}-1`); + const secondaryInput = new TestFileEditorInput(URI.file('foo/bar-secondary'), TEST_EDITOR_INPUT_ID); + const primaryInput = new TestFileEditorInput(URI.file('foo/bar-primary'), `${TEST_EDITOR_INPUT_ID}-1`); - const sideBySideEditor = new SideBySideEditorInput(undefined, undefined, input1, input2, accessor.editorService); + const sideBySideEditor = new SideBySideEditorInput(undefined, undefined, secondaryInput, primaryInput, accessor.editorService); await group.openEditor(sideBySideEditor, { pinned: true }); - let foundEditors = group.findEditors(URI.file('foo/bar1')); + let foundEditors = group.findEditors(URI.file('foo/bar-secondary')); assert.strictEqual(foundEditors.length, 0); - foundEditors = group.findEditors(URI.file('foo/bar1'), { supportSideBySide: SideBySideEditor.PRIMARY }); + foundEditors = group.findEditors(URI.file('foo/bar-secondary'), { supportSideBySide: SideBySideEditor.PRIMARY }); + assert.strictEqual(foundEditors.length, 0); + + foundEditors = group.findEditors(URI.file('foo/bar-primary'), { supportSideBySide: SideBySideEditor.PRIMARY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = group.findEditors(URI.file('foo/bar-secondary'), { supportSideBySide: SideBySideEditor.SECONDARY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = group.findEditors(URI.file('foo/bar-primary'), { supportSideBySide: SideBySideEditor.SECONDARY }); + assert.strictEqual(foundEditors.length, 0); + + foundEditors = group.findEditors(URI.file('foo/bar-secondary'), { supportSideBySide: SideBySideEditor.ANY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = group.findEditors(URI.file('foo/bar-primary'), { supportSideBySide: SideBySideEditor.ANY }); assert.strictEqual(foundEditors.length, 1); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index aa5b76db0a4..693c94059a6 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -2369,6 +2369,45 @@ suite('EditorService', () => { registrationDisposable.dispose(); }); + test('closeEditor', async () => { + const [part, service] = await createEditorService(); + + const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + // Open editors + await service.openEditors([{ editor: input, options: { override: EditorResolution.DISABLED } }, { editor: otherInput, options: { override: EditorResolution.DISABLED } }]); + assert.strictEqual(part.activeGroup.count, 2); + + // Close editor + await service.closeEditor({ editor: input, groupId: part.activeGroup.id }); + assert.strictEqual(part.activeGroup.count, 1); + + await service.closeEditor({ editor: input, groupId: part.activeGroup.id }); + assert.strictEqual(part.activeGroup.count, 1); + + await service.closeEditor({ editor: otherInput, groupId: part.activeGroup.id }); + assert.strictEqual(part.activeGroup.count, 0); + + await service.closeEditor({ editor: otherInput, groupId: 999 }); + assert.strictEqual(part.activeGroup.count, 0); + }); + + test('closeEditors', async () => { + const [part, service] = await createEditorService(); + + const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + + // Open editors + await service.openEditors([{ editor: input, options: { override: EditorResolution.DISABLED } }, { editor: otherInput, options: { override: EditorResolution.DISABLED } }]); + assert.strictEqual(part.activeGroup.count, 2); + + // Close editors + await service.closeEditors([{ editor: input, groupId: part.activeGroup.id }, { editor: otherInput, groupId: part.activeGroup.id }]); + assert.strictEqual(part.activeGroup.count, 0); + }); + test('findEditors (in group)', async () => { const [part, service] = await createEditorService(); @@ -2492,17 +2531,32 @@ suite('EditorService', () => { test('findEditors (support side by side via options)', async () => { const [, service] = await createEditorService(); - const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); - const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const secondaryInput = new TestFileEditorInput(URI.parse('my://resource-findEditors-secondary'), TEST_EDITOR_INPUT_ID); + const primaryInput = new TestFileEditorInput(URI.parse('my://resource-findEditors-primary'), TEST_EDITOR_INPUT_ID); - const sideBySideInput = new SideBySideEditorInput(undefined, undefined, input, otherInput, service); + const sideBySideInput = new SideBySideEditorInput(undefined, undefined, secondaryInput, primaryInput, service); await service.openEditor(sideBySideInput, { pinned: true }); - let foundEditors = service.findEditors(URI.parse('my://resource2-openEditors')); + let foundEditors = service.findEditors(URI.parse('my://resource-findEditors-primary')); assert.strictEqual(foundEditors.length, 0); - foundEditors = service.findEditors(URI.parse('my://resource2-openEditors'), { supportSideBySide: SideBySideEditor.PRIMARY }); + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-primary'), { supportSideBySide: SideBySideEditor.PRIMARY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-secondary'), { supportSideBySide: SideBySideEditor.PRIMARY }); + assert.strictEqual(foundEditors.length, 0); + + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-primary'), { supportSideBySide: SideBySideEditor.SECONDARY }); + assert.strictEqual(foundEditors.length, 0); + + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-secondary'), { supportSideBySide: SideBySideEditor.SECONDARY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-primary'), { supportSideBySide: SideBySideEditor.ANY }); + assert.strictEqual(foundEditors.length, 1); + + foundEditors = service.findEditors(URI.parse('my://resource-findEditors-secondary'), { supportSideBySide: SideBySideEditor.ANY }); assert.strictEqual(foundEditors.length, 1); }); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 84c8f48f3fc..0028c843a15 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -52,7 +52,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get logFile(): URI { return joinPath(this.logsHome, 'window.log'); } @memoize - get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); } + get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.vscodeUserData }); } @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } @@ -67,10 +67,13 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get cacheHome(): URI { return joinPath(this.userRoamingDataHome, 'caches'); } @memoize - get globalStorageHome(): URI { return URI.joinPath(this.userRoamingDataHome, 'globalStorage'); } + get globalStorageHome(): URI { return joinPath(this.userRoamingDataHome, 'globalStorage'); } @memoize - get workspaceStorageHome(): URI { return URI.joinPath(this.userRoamingDataHome, 'workspaceStorage'); } + get workspaceStorageHome(): URI { return joinPath(this.userRoamingDataHome, 'workspaceStorage'); } + + @memoize + get localHistoryHome(): URI { return joinPath(this.userRoamingDataHome, 'History'); } /** * In Web every workspace can potentially have scoped user-data @@ -182,7 +185,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi const webviewExternalEndpointCommit = this.payload?.get('webviewExternalEndpointCommit'); return endpoint - .replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b') + .replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '181b43c0e2949e36ecb623d8cc6de29d4fa2bae8') .replace('{{quality}}', (webviewExternalEndpointCommit ? 'insider' : this.productService.quality) ?? 'insider'); } @@ -205,7 +208,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi get skipWelcome(): boolean { return this.payload?.get('skipWelcome') === 'true'; } @memoize - get disableWorkspaceTrust(): boolean { return true; } + get disableWorkspaceTrust(): boolean { return !this.options.enableWorkspaceTrust; } private payload: Map | undefined; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 091a6452a1b..ab7626e36cb 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -82,7 +82,7 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment } @memoize - override get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); } + override get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.vscodeUserData }); } @memoize get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.configuration.windowId}.log`)); } diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 5eb9d09bf2e..c88dede88ba 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionManifest, IExtension } from 'vs/platform/extensions/common/extensions'; +import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionManifest, IExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isWeb } from 'vs/base/common/platform'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -57,16 +57,14 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne manifest: e.packageNLS ? localizeManifest(e.packageJSON, e.packageNLS) : e.packageJSON, readmeUrl: e.readmePath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.readmePath) : undefined, changelogUrl: e.changelogPath ? uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.changelogPath) : undefined, + targetPlatform: TargetPlatform.WEB, })); } } } async scanBuiltinExtensions(): Promise { - if (isWeb) { - return this.builtinExtensions; - } - throw new Error('not supported'); + return [...this.builtinExtensions]; } } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts index 141ac701245..1fb6a1411e5 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts @@ -226,7 +226,7 @@ registerAction2(class extends Action2 { id: MenuId.ViewContainerTitle, when: ContextKeyExpr.equals('viewContainer', 'workbench.view.extensions'), group: '2_enablement', - order: 3 + order: 4 } }); } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index ebd50ecda8b..2bdb8b94008 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { IBuiltinExtensionsScannerService, ExtensionType, IExtensionIdentifier, IExtension, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { isWeb } from 'vs/base/common/platform'; @@ -15,7 +15,7 @@ import { Queue } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IExtensionGalleryService, IGalleryExtension, IGalleryMetadata, Metadata, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionGalleryService, IExtensionInfo, IGalleryExtension, IGalleryMetadata, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { groupByExtension, areSameExtensions, getGalleryExtensionId, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Disposable } from 'vs/base/common/lifecycle'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; @@ -33,8 +33,12 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { basename } from 'vs/base/common/path'; import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; +type ExtensionInfo = { readonly id: string; preRelease: boolean }; function isGalleryExtensionInfo(obj: unknown): obj is GalleryExtensionInfo { const galleryExtensionInfo = obj as GalleryExtensionInfo | undefined; @@ -67,9 +71,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten declare readonly _serviceBrand: undefined; - private readonly builtinExtensionsPromise: Promise = Promise.resolve([]); - private readonly customBuiltinExtensionsPromise: Promise = Promise.resolve([]); - + private readonly systemExtensionsCacheResource: URI | undefined = undefined; private readonly customBuiltinExtensionsCacheResource: URI | undefined = undefined; private readonly installedExtensionsResource: URI | undefined = undefined; private readonly resourcesAccessQueueMap = new ResourceMap>(); @@ -83,95 +85,52 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IExtensionStorageService private readonly extensionStorageService: IExtensionStorageService, + @IStorageService private readonly storageService: IStorageService, + @ILifecycleService lifecycleService: ILifecycleService, ) { super(); if (isWeb) { this.installedExtensionsResource = joinPath(environmentService.userRoamingDataHome, 'extensions.json'); + this.systemExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'systemExtensionsCache.json'); this.customBuiltinExtensionsCacheResource = joinPath(environmentService.userRoamingDataHome, 'customBuiltinExtensionsCache.json'); - this.builtinExtensionsPromise = this.readSystemExtensions(); - this.customBuiltinExtensionsPromise = this.readCustomBuiltinExtensions(); this.registerActions(); + + // Eventually update caches + lifecycleService.when(LifecyclePhase.Eventually).then(() => this.updateCaches()); } } - /** - * All system extensions bundled with the product - */ - private async readSystemExtensions(): Promise { - return this.builtinExtensionsScannerService.scanBuiltinExtensions(); - } - - /** - * All extensions defined via `additionalBuiltinExtensions` API - */ - private async readCustomBuiltinExtensions(): Promise { - const extensions: { id: string; preRelease: boolean }[] = [], extensionLocations: URI[] = [], result: IExtension[] = []; - const extensionsToMigrate: [string, string][] = []; - const cutomBuiltinExtensions: (GalleryExtensionInfo | UriComponents)[] = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions) - ? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension) - : []; - for (const e of cutomBuiltinExtensions) { - if (isGalleryExtensionInfo(e)) { - extensions.push({ id: e.id, preRelease: !!e.preRelease }); - if (e.migrateStorageFrom) { - extensionsToMigrate.push([e.migrateStorageFrom, e.id]); - } - } else { - extensionLocations.push(URI.revive(e)); - } - } - - await Promise.allSettled([ - (async () => { - if (extensionLocations.length) { - await Promise.allSettled(extensionLocations.map(async location => { - try { - const webExtension = await this.toWebExtension(location); - result.push(await this.toScannedExtension(webExtension, true)); - } catch (error) { - this.logService.info(`Error while fetching the additional builtin extension ${location.toString()}.`, getErrorMessage(error)); + private _customBuiltinExtensionsInfoPromise: Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[] }> | undefined; + private readCustomBuiltinExtensionsInfoFromEnv(): Promise<{ extensions: ExtensionInfo[]; extensionsToMigrate: [string, string][]; extensionLocations: URI[] }> { + if (!this._customBuiltinExtensionsInfoPromise) { + this._customBuiltinExtensionsInfoPromise = (async () => { + let extensions: ExtensionInfo[] = [], extensionLocations: URI[] = []; + const extensionsToMigrate: [string, string][] = []; + const customBuiltinExtensionsInfo = this.environmentService.options && Array.isArray(this.environmentService.options.additionalBuiltinExtensions) + ? this.environmentService.options.additionalBuiltinExtensions.map(additionalBuiltinExtension => isString(additionalBuiltinExtension) ? { id: additionalBuiltinExtension } : additionalBuiltinExtension) + : []; + for (const e of customBuiltinExtensionsInfo) { + if (isGalleryExtensionInfo(e)) { + extensions.push({ id: e.id, preRelease: !!e.preRelease }); + if (e.migrateStorageFrom) { + extensionsToMigrate.push([e.migrateStorageFrom, e.id]); } - })); - } - })(), - (async () => { - if (extensions.length) { - try { - result.push(...await this.getCustomBuiltinExtensionsFromGallery(extensions)); - } catch (error) { - this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error)); - } - } else { - await this.writeCustomBuiltinExtensionsCache(() => []); - } - })(), - ]); - - if (extensionsToMigrate.length) { - const fromExtensions = await this.galleryService.getExtensions(extensionsToMigrate.map(([id]) => ({ id })), CancellationToken.None); - try { - await Promise.allSettled(extensionsToMigrate.map(async ([from, to]) => { - const toExtension = result.find(extension => areSameExtensions(extension.identifier, { id: to })); - if (toExtension) { - const fromExtension = fromExtensions.find(extension => areSameExtensions(extension.identifier, { id: from })); - const fromExtensionManifest = fromExtension ? await this.galleryService.getManifest(fromExtension, CancellationToken.None) : null; - const fromExtensionId = fromExtensionManifest ? getExtensionId(fromExtensionManifest.publisher, fromExtensionManifest.name) : from; - const toExtensionId = getExtensionId(toExtension.manifest.publisher, toExtension.manifest.name); - this.extensionStorageService.addToMigrationList(fromExtensionId, toExtensionId); } else { - this.logService.info(`Skipped migrating extension storage from '${from}' to '${to}', because the '${to}' extension is not found.`); + extensionLocations.push(URI.revive(e)); } - })); - } catch (error) { - this.logService.error(error); - } + } + if (extensions.length) { + extensions = await this.checkAdditionalBuiltinExtensions(extensions); + } + return { extensions, extensionsToMigrate, extensionLocations }; + })(); } - return result; + return this._customBuiltinExtensionsInfoPromise; } - private async checkAdditionalBuiltinExtensions(extensions: { id: string; preRelease: boolean }[]): Promise<{ id: string; preRelease: boolean }[]> { + private async checkAdditionalBuiltinExtensions(extensions: ExtensionInfo[]): Promise { const extensionsControlManifest = await this.galleryService.getExtensionsControlManifest(); - const result: { id: string; preRelease: boolean }[] = []; + const result: ExtensionInfo[] = []; for (const extension of extensions) { if (extensionsControlManifest.malicious.some(e => areSameExtensions(e, { id: extension.id }))) { this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`); @@ -188,80 +147,214 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return result; } - private async getCustomBuiltinExtensionsFromGallery(extensions: { id: string; preRelease: boolean }[]): Promise { + /** + * All system extensions bundled with the product + */ + private async readSystemExtensions(): Promise { + const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); + const cachedSystemExtensions = await Promise.all((await this.readSystemExtensionsCache()).map(e => this.toScannedExtension(e, true, ExtensionType.System))); + + const result = new Map(); + for (const extension of [...systemExtensions, ...cachedSystemExtensions]) { + const existing = result.get(extension.identifier.id.toLowerCase()); + if (existing) { + // Incase there are duplicates always take the latest version + if (semver.gt(existing.manifest.version, extension.manifest.version)) { + continue; + } + } + result.set(extension.identifier.id.toLowerCase(), extension); + } + return [...result.values()]; + } + + /** + * All extensions defined via `additionalBuiltinExtensions` API + */ + private async readCustomBuiltinExtensions(): Promise { + const [customBuiltinExtensionsFromLocations, customBuiltinExtensionsFromGallery] = await Promise.all([ + this.getCustomBuiltinExtensionsFromLocations(), + this.getCustomBuiltinExtensionsFromGallery(), + ]); + const customBuiltinExtensions: IExtension[] = [...customBuiltinExtensionsFromLocations, ...customBuiltinExtensionsFromGallery]; + await this.migrateExtensionsStorage(customBuiltinExtensions); + return customBuiltinExtensions; + } + + private async getCustomBuiltinExtensionsFromLocations(): Promise { + const { extensionLocations } = await this.readCustomBuiltinExtensionsInfoFromEnv(); + if (!extensionLocations.length) { + return []; + } + const result: IExtension[] = []; + await Promise.allSettled(extensionLocations.map(async location => { + try { + const webExtension = await this.toWebExtension(location); + result.push(await this.toScannedExtension(webExtension, true)); + } catch (error) { + this.logService.info(`Error while fetching the additional builtin extension ${location.toString()}.`, getErrorMessage(error)); + } + })); + return result; + } + + private async getCustomBuiltinExtensionsFromGallery(): Promise { if (!this.galleryService.isEnabled()) { this.logService.info('Ignoring fetching additional builtin extensions from gallery as it is disabled.'); return []; } - - let cachedStaticWebExtensions = await this.readCustomBuiltinExtensionsCache(); - - // Incase there are duplicates always take the latest version - const byExtension: IWebExtension[][] = groupByExtension(cachedStaticWebExtensions, e => e.identifier); - cachedStaticWebExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); - - const webExtensions: IWebExtension[] = []; - extensions = extensions.map(e => ({ ...e, id: e.id.toLowerCase() })); - - for (const webExtension of cachedStaticWebExtensions) { - const index = extensions.findIndex(e => e.id === webExtension.identifier.id.toLowerCase() && e.preRelease === webExtension.metadata?.isPreReleaseVersion); - if (index !== -1) { - webExtensions.push(webExtension); - /* Update preRelease flag in the cache - https://github.com/microsoft/vscode/issues/142831 */ - if (webExtension.metadata?.isPreReleaseVersion && !webExtension.metadata?.preRelease) { - webExtension.metadata.preRelease = true; - } - extensions.splice(index, 1); - } + const { extensions } = await this.readCustomBuiltinExtensionsInfoFromEnv(); + if (!extensions.length) { + return []; } - - if (extensions.length) { - extensions = (await this.checkAdditionalBuiltinExtensions(extensions)); - const galleryExtensions = await this.galleryService.getExtensions(extensions, CancellationToken.None); - const missingExtensions = extensions.filter(({ id }) => !galleryExtensions.find(({ identifier }) => areSameExtensions(identifier, { id }))); - if (missingExtensions.length) { - this.logService.info('Cannot find static extensions from gallery', missingExtensions); - } - - await Promise.all(galleryExtensions.map(async gallery => { - try { - webExtensions.push(await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true })); - } catch (error) { - this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error)); - } - })); - } - const result: IExtension[] = []; - - if (webExtensions.length) { - await Promise.all(webExtensions.map(async webExtension => { - try { - result.push(await this.toScannedExtension(webExtension, true)); - } catch (error) { - this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error)); - } - })); - } - try { - await this.writeCustomBuiltinExtensionsCache(() => webExtensions); + const useCache = this.storageService.get('additionalBuiltinExtensions', StorageScope.GLOBAL, '[]') === JSON.stringify(extensions); + const webExtensions = await (useCache ? this.getCustomBuiltinExtensionsFromCache() : this.updateCustomBuiltinExtensionsCache()); + if (webExtensions.length) { + await Promise.all(webExtensions.map(async webExtension => { + try { + result.push(await this.toScannedExtension(webExtension, true)); + } catch (error) { + this.logService.info(`Ignoring additional builtin extension ${webExtension.identifier.id} because there is an error while converting it into scanned extension`, getErrorMessage(error)); + } + })); + } + this.storageService.store('additionalBuiltinExtensions', JSON.stringify(extensions), StorageScope.GLOBAL, StorageTarget.MACHINE); } catch (error) { - this.logService.info(`Ignoring the error while adding additional builtin gallery extensions`, getErrorMessage(error)); + this.logService.info('Ignoring following additional builtin extensions as there is an error while fetching them from gallery', extensions.map(({ id }) => id), getErrorMessage(error)); } - return result; } + private async getCustomBuiltinExtensionsFromCache(): Promise { + const cachedCustomBuiltinExtensions = await this.readCustomBuiltinExtensionsCache(); + const webExtensionsMap = new Map(); + for (const webExtension of cachedCustomBuiltinExtensions) { + const existing = webExtensionsMap.get(webExtension.identifier.id.toLowerCase()); + if (existing) { + // Incase there are duplicates always take the latest version + if (semver.gt(existing.version, webExtension.version)) { + continue; + } + } + /* Update preRelease flag in the cache - https://github.com/microsoft/vscode/issues/142831 */ + if (webExtension.metadata?.isPreReleaseVersion && !webExtension.metadata?.preRelease) { + webExtension.metadata.preRelease = true; + } + webExtensionsMap.set(webExtension.identifier.id.toLowerCase(), webExtension); + } + return [...webExtensionsMap.values()]; + } + + private _migrateExtensionsStoragePromise: Promise | undefined; + private async migrateExtensionsStorage(customBuiltinExtensions: IExtension[]): Promise { + if (!this._migrateExtensionsStoragePromise) { + this._migrateExtensionsStoragePromise = (async () => { + const { extensionsToMigrate } = await this.readCustomBuiltinExtensionsInfoFromEnv(); + if (!extensionsToMigrate.length) { + return; + } + const fromExtensions = await this.galleryService.getExtensions(extensionsToMigrate.map(([id]) => ({ id })), CancellationToken.None); + try { + await Promise.allSettled(extensionsToMigrate.map(async ([from, to]) => { + const toExtension = customBuiltinExtensions.find(extension => areSameExtensions(extension.identifier, { id: to })); + if (toExtension) { + const fromExtension = fromExtensions.find(extension => areSameExtensions(extension.identifier, { id: from })); + const fromExtensionManifest = fromExtension ? await this.galleryService.getManifest(fromExtension, CancellationToken.None) : null; + const fromExtensionId = fromExtensionManifest ? getExtensionId(fromExtensionManifest.publisher, fromExtensionManifest.name) : from; + const toExtensionId = getExtensionId(toExtension.manifest.publisher, toExtension.manifest.name); + this.extensionStorageService.addToMigrationList(fromExtensionId, toExtensionId); + } else { + this.logService.info(`Skipped migrating extension storage from '${from}' to '${to}', because the '${to}' extension is not found.`); + } + })); + } catch (error) { + this.logService.error(error); + } + })(); + } + return this._migrateExtensionsStoragePromise; + } + + private async updateCaches(): Promise { + await this.updateSystemExtensionsCache(); + await this.updateCustomBuiltinExtensionsCache(); + } + + private async updateSystemExtensionsCache(): Promise { + const systemExtensions = await this.builtinExtensionsScannerService.scanBuiltinExtensions(); + const cachedSystemExtensions = (await this.readSystemExtensionsCache()) + .filter(cached => { + const systemExtension = systemExtensions.find(e => areSameExtensions(e.identifier, cached.identifier)); + return systemExtension && semver.gt(cached.version, systemExtension.manifest.version); + }); + await this.writeSystemExtensionsCache(() => cachedSystemExtensions); + } + + private _updateCustomBuiltinExtensionsCachePromise: Promise | undefined; + private async updateCustomBuiltinExtensionsCache(): Promise { + if (!this._updateCustomBuiltinExtensionsCachePromise) { + this._updateCustomBuiltinExtensionsCachePromise = (async () => { + // Clear Cache + await this.writeCustomBuiltinExtensionsCache(() => []); + + const { extensions } = await this.readCustomBuiltinExtensionsInfoFromEnv(); + + if (!extensions.length) { + return []; + } + + const galleryExtensionsMap = await this.getExtensionsWithDependenciesAndPackedExtensions(extensions); + + const missingExtensions = extensions.filter(({ id }) => !galleryExtensionsMap.has(id.toLowerCase())); + if (missingExtensions.length) { + this.logService.info('Skipping the additional builtin extensions because their compatible versions are not foud.', missingExtensions); + } + + const webExtensions: IWebExtension[] = []; + await Promise.all([...galleryExtensionsMap.values()].map(async gallery => { + try { + webExtensions.push(await this.toWebExtensionFromGallery(gallery, { isPreReleaseVersion: gallery.properties.isPreReleaseVersion, preRelease: gallery.properties.isPreReleaseVersion, isBuiltin: true })); + } catch (error) { + this.logService.info(`Ignoring additional builtin extension ${gallery.identifier.id} because there is an error while converting it into web extension`, getErrorMessage(error)); + } + })); + + await this.writeCustomBuiltinExtensionsCache(() => webExtensions); + return webExtensions; + })(); + } + return this._updateCustomBuiltinExtensionsCachePromise; + } + + private async getExtensionsWithDependenciesAndPackedExtensions(toGet: IExtensionInfo[], result: Map = new Map()): Promise> { + if (toGet.length === 0) { + return result; + } + const extensions = await this.galleryService.getExtensions(toGet, { compatible: true, targetPlatform: TargetPlatform.WEB }, CancellationToken.None); + const packsAndDependencies = new Map(); + for (const extension of extensions) { + result.set(extension.identifier.id.toLowerCase(), extension); + for (const id of [...(isNonEmptyArray(extension.properties.dependencies) ? extension.properties.dependencies : []), ...(isNonEmptyArray(extension.properties.extensionPack) ? extension.properties.extensionPack : [])]) { + if (!result.has(id.toLowerCase()) && !packsAndDependencies.has(id.toLowerCase())) { + const extensionInfo = toGet.find(e => areSameExtensions(e, extension.identifier)); + packsAndDependencies.set(id.toLowerCase(), { id, preRelease: extensionInfo?.preRelease }); + } + } + } + return this.getExtensionsWithDependenciesAndPackedExtensions([...packsAndDependencies.values()].filter(({ id }) => !result.has(id.toLowerCase())), result); + } + async scanSystemExtensions(): Promise { - return this.builtinExtensionsPromise; + return this.readSystemExtensions(); } async scanUserExtensions(donotIgnoreInvalidExtensions?: boolean): Promise { const extensions = new Map(); // Custom builtin extensions defined through `additionalBuiltinExtensions` API - const customBuiltinExtensions = await this.customBuiltinExtensionsPromise; + const customBuiltinExtensions = await this.readCustomBuiltinExtensions(); for (const extension of customBuiltinExtensions) { extensions.set(extension.identifier.id.toLowerCase(), extension); } @@ -332,10 +425,21 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten await this.writeInstalledExtensions(installedExtensions => installedExtensions.filter(extension => !(areSameExtensions(extension.identifier, identifier) && (version ? extension.version === version : true)))); } - private async addWebExtension(webExtension: IWebExtension) { - const isBuiltin = (await this.customBuiltinExtensionsPromise).some(builtinExtension => areSameExtensions(webExtension.identifier, builtinExtension.identifier)); + private async addWebExtension(webExtension: IWebExtension): Promise { + const isSystem = !!(await this.scanSystemExtensions()).find(e => areSameExtensions(e.identifier, webExtension.identifier)); + const isBuiltin = !!webExtension.metadata?.isBuiltin; const extension = await this.toScannedExtension(webExtension, isBuiltin); + if (isSystem) { + await this.writeSystemExtensionsCache(systemExtensions => { + // Remove the existing extension to avoid duplicates + systemExtensions = systemExtensions.filter(extension => !areSameExtensions(extension.identifier, webExtension.identifier)); + systemExtensions.push(webExtension); + return systemExtensions; + }); + return extension; + } + // Update custom builtin extensions to custom builtin extensions cache if (isBuiltin) { await this.writeCustomBuiltinExtensionsCache(customBuiltinExtensions => { @@ -350,13 +454,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten if (installedExtensions.some(e => areSameExtensions(e.identifier, webExtension.identifier))) { await this.addToInstalledExtensions(webExtension); } + return extension; } // Add to installed extensions - else { - await this.addToInstalledExtensions(webExtension); - } - + await this.addToInstalledExtensions(webExtension); return extension; } @@ -436,7 +538,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }; } - private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean): Promise { + private async toScannedExtension(webExtension: IWebExtension, isBuiltin: boolean, type: ExtensionType = ExtensionType.User): Promise { const url = joinPath(webExtension.location, 'package.json'); let content; @@ -461,11 +563,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten identifier: { id: webExtension.identifier.id, uuid: webExtension.identifier.uuid || uuid }, location: webExtension.location, manifest, - type: ExtensionType.User, + type, isBuiltin, readmeUrl: webExtension.readmeUri, changelogUrl: webExtension.changelogUri, - metadata: webExtension.metadata + metadata: webExtension.metadata, + targetPlatform: TargetPlatform.WEB }; } @@ -505,6 +608,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return this.withWebExtensions(this.customBuiltinExtensionsCacheResource, updateFn); } + private readSystemExtensionsCache(): Promise { + return this.withWebExtensions(this.systemExtensionsCacheResource); + } + + private writeSystemExtensionsCache(updateFn: (extensions: IWebExtension[]) => IWebExtension[]): Promise { + return this.withWebExtensions(this.systemExtensionsCacheResource, updateFn); + } + private async withWebExtensions(file: URI | undefined, updateFn?: (extensions: IWebExtension[]) => IWebExtension[]): Promise { if (!file) { return []; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index e67a7b7e5a3..e7282ff5733 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -6,9 +6,8 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtension, ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IGalleryExtension, IExtensionIdentifier, ILocalExtension, InstallOptions, InstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionResult, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; -import { IStringDictionary } from 'vs/base/common/collections'; import { FileAccess } from 'vs/base/common/network'; export interface IExtensionManagementServer { @@ -140,7 +139,7 @@ export interface IWorkbenchExtensionEnablementService { } export interface IScannedExtension extends IExtension { - readonly metadata?: IStringDictionary; + readonly metadata?: Metadata; } export const IWebExtensionsScannerService = createDecorator('IWebExtensionsScannerService'); @@ -152,8 +151,8 @@ export interface IWebExtensionsScannerService { scanExtensionsUnderDevelopment(): Promise; scanExistingExtension(extensionLocation: URI, extensionType: ExtensionType): Promise; - addExtension(location: URI, metadata?: IStringDictionary): Promise; - addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: IStringDictionary): Promise; + addExtension(location: URI, metadata?: Metadata): Promise; + addExtensionFromGallery(galleryExtension: IGalleryExtension, metadata?: Metadata): Promise; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise; scanExtensionManifest(extensionLocation: URI): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 538950c4c25..1b34c6a0604 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -5,15 +5,15 @@ import { Event, EventMultiplexer } from 'vs/base/common/event'; import { - ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, TargetPlatform, ExtensionManagementError, ExtensionManagementErrorCode + ILocalExtension, IGalleryExtension, IExtensionIdentifier, IExtensionsControlManifest, IGalleryMetadata, IExtensionGalleryService, InstallOptions, UninstallOptions, InstallVSIXOptions, InstallExtensionResult, ExtensionManagementError, ExtensionManagementErrorCode } from 'vs/platform/extensionManagement/common/extensionManagement'; import { DidUninstallExtensionOnServerEvent, IExtensionManagementServer, IExtensionManagementServerService, InstallExtensionOnServerEvent, IWorkbenchExtensionManagementService, UninstallExtensionOnServerEvent } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, isLanguagePackExtension, IExtensionManifest, getWorkspaceSupportTypeMessage, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, computeTargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; @@ -29,6 +29,8 @@ import { IExtensionManifestPropertiesService } from 'vs/workbench/services/exten import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { isUndefined } from 'vs/base/common/types'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { @@ -51,6 +53,8 @@ export class ExtensionManagementService extends Disposable implements IWorkbench @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -462,6 +466,13 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } + private _targetPlatformPromise: Promise | undefined; + getTargetPlatform(): Promise { + if (!this._targetPlatformPromise) { + this._targetPlatformPromise = computeTargetPlatform(this.fileService, this.logService); + } + return this._targetPlatformPromise; + } + registerParticipant() { throw new Error('Not Supported'); } - getTargetPlatform(): Promise { throw new Error('Not Supported'); } } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index c8210fb8179..d319bfd7689 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionType, IExtensionIdentifier, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata, TargetPlatform } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionIdentifier, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IGalleryMetadata, InstallOperation, IExtensionGalleryService, InstallOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IScannedExtension, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -14,7 +14,7 @@ import { AbstractExtensionManagementService, AbstractExtensionTask, IInstallExte import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IProductService } from 'vs/platform/product/common/productService'; -import { isBoolean } from 'vs/base/common/types'; +import { isBoolean, isUndefined } from 'vs/base/common/types'; export class WebExtensionManagementService extends AbstractExtensionManagementService implements IExtensionManagementService { @@ -111,7 +111,9 @@ function toLocalExtension(extension: IScannedExtension): ILocalExtension { publisherDisplayName: metadata.publisherDisplayName || null, installedTimestamp: metadata.installedTimestamp, isPreReleaseVersion: !!metadata.isPreReleaseVersion, - preRelease: !!metadata.preRelease + preRelease: !!metadata.preRelease, + targetPlatform: TargetPlatform.WEB, + updated: !!metadata.updated }; } @@ -125,8 +127,9 @@ class InstallExtensionTask extends AbstractExtensionTask implem readonly identifier: IExtensionIdentifier; readonly source: URI | IGalleryExtension; + private _operation = InstallOperation.Install; - get operation() { return this._operation; } + get operation() { return isUndefined(this.options.operation) ? this._operation : this.options.operation; } constructor( manifest: IExtensionManifest, @@ -153,6 +156,9 @@ class InstallExtensionTask extends AbstractExtensionTask implem metadata.publisherId = this.extension.publisherId; metadata.installedTimestamp = Date.now(); metadata.isPreReleaseVersion = this.extension.properties.isPreReleaseVersion; + metadata.isBuiltin = this.options.isBuiltin || existingExtension?.isBuiltin; + metadata.isSystem = existingExtension?.type === ExtensionType.System ? true : undefined; + metadata.updated = !!existingExtension; metadata.preRelease = this.extension.properties.isPreReleaseVersion || (isBoolean(this.options.installPreReleaseVersion) ? this.options.installPreReleaseVersion /* Respect the passed flag */ diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts index a1f71634024..007e0a3fc73 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementServerService.ts @@ -6,7 +6,6 @@ import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; import { ExtensionInstallLocation, IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; @@ -15,6 +14,7 @@ import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/ex import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; export class ExtensionManagementServerService implements IExtensionManagementServerService { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index 73dc702c542..f1b1bffb3a3 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -20,6 +20,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtensionManagementService extends BaseExtensionManagementService { @@ -34,9 +36,11 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @IDialogService dialogService: IDialogService, @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService, @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, + @IFileService fileService: IFileService, + @ILogService logService: ILogService, @IInstantiationService instantiationService: IInstantiationService, ) { - super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataSyncEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService, instantiationService); + super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataSyncEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService, fileService, logService, instantiationService); } protected override async installVSIX(vsix: URI, server: IExtensionManagementServer, options: InstallVSIXOptions | undefined): Promise { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 60b51cb441e..2f041ddabcc 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -9,14 +9,14 @@ import { IWorkbenchExtensionEnablementService, IWebExtensionsScannerService } fr import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionService, IExtensionHost, toExtensionDescription, ExtensionRunningLocation, extensionRunningLocationToString } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, IExtensionHost, toExtensionDescription, ExtensionRunningLocation, ExtensionHostKind, extensionHostKindToString } from 'vs/workbench/services/extensions/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IProductService } from 'vs/platform/product/common/productService'; import { AbstractExtensionService, ExtensionRunningPreference, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import { RemoteExtensionHost, IRemoteExtensionHostDataProvider, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionIdentifier, IExtensionDescription, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -73,8 +73,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten remoteAgentService ); - this._runningLocation = new Map(); - // Initialize installed extensions first and do it only after workbench is ready this._lifecycleService.when(LifecyclePhase.Ready).then(async () => { await this._userDataInitializationService.initializeInstalledExtensions(this._instantiationService); @@ -108,11 +106,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._disposables.add(this._fileService.registerProvider(Schemas.https, provider)); } - private _createLocalExtensionHostDataProvider() { + private _createLocalExtensionHostDataProvider(desiredRunningLocation: ExtensionRunningLocation): IWebWorkerExtensionHostDataProvider { return { getInitData: async () => { const allExtensions = await this.getExtensions(); - const localWebWorkerExtensions = filterByRunningLocation(allExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); + const localWebWorkerExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, extensions: localWebWorkerExtensions @@ -131,20 +129,20 @@ export class ExtensionService extends AbstractExtensionService implements IExten }; } - protected _pickRunningLocation(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionRunningLocation { + protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { const result = ExtensionService.pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely, preference); - this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionRunningLocationToString(result)}`); + this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionHostKindToString(result)}`); return result; } - public static pickRunningLocation(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionRunningLocation { - const result: ExtensionRunningLocation[] = []; + public static pickRunningLocation(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { + const result: ExtensionHostKind[] = []; let canRunRemotely = false; for (const extensionKind of extensionKinds) { if (extensionKind === 'ui' && isInstalledRemotely) { // ui extensions run remotely if possible (but only as a last resort) if (preference === ExtensionRunningPreference.Remote) { - return ExtensionRunningLocation.Remote; + return ExtensionHostKind.Remote; } else { canRunRemotely = true; } @@ -152,39 +150,42 @@ export class ExtensionService extends AbstractExtensionService implements IExten if (extensionKind === 'workspace' && isInstalledRemotely) { // workspace extensions run remotely if possible if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Remote) { - return ExtensionRunningLocation.Remote; + return ExtensionHostKind.Remote; } else { - result.push(ExtensionRunningLocation.Remote); + result.push(ExtensionHostKind.Remote); } } if (extensionKind === 'web' && (isInstalledLocally || isInstalledRemotely)) { // web worker extensions run in the local web worker if possible if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Local) { - return ExtensionRunningLocation.LocalWebWorker; + return ExtensionHostKind.LocalWebWorker; } else { - result.push(ExtensionRunningLocation.LocalWebWorker); + result.push(ExtensionHostKind.LocalWebWorker); } } } if (canRunRemotely) { - result.push(ExtensionRunningLocation.Remote); + result.push(ExtensionHostKind.Remote); } - return (result.length > 0 ? result[0] : ExtensionRunningLocation.None); + return (result.length > 0 ? result[0] : null); } - protected _createExtensionHosts(_isInitialStart: boolean): IExtensionHost[] { - const result: IExtensionHost[] = []; - - const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, false, this._createLocalExtensionHostDataProvider()); - result.push(webWorkerExtHost); - - const remoteAgentConnection = this._remoteAgentService.getConnection(); - if (remoteAgentConnection) { - const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); - result.push(remoteExtHost); + protected _createExtensionHost(runningLocation: ExtensionRunningLocation, _isInitialStart: boolean): IExtensionHost | null { + switch (runningLocation.kind) { + case ExtensionHostKind.LocalProcess: { + return null; + } + case ExtensionHostKind.LocalWebWorker: { + return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, false, this._createLocalExtensionHostDataProvider(runningLocation)); + } + case ExtensionHostKind.Remote: { + const remoteAgentConnection = this._remoteAgentService.getConnection(); + if (remoteAgentConnection) { + return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + } + return null; + } } - - return result; } protected async _scanAndHandleExtensions(): Promise { @@ -200,12 +201,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten const remoteAgentConnection = this._remoteAgentService.getConnection(); // `determineRunningLocation` will look at the complete picture (e.g. an extension installed on both sides), // takes care of duplicates and picks a running location for each extension - this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); + this._initializeRunningLocation(localExtensions, remoteExtensions); // Some remote extensions could run locally in the web worker, so store them - const remoteExtensionsThatNeedToRunLocally = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); - localExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); - remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote); + const remoteExtensionsThatNeedToRunLocally = this._filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.LocalWebWorker); + localExtensions = this._filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalWebWorker); + remoteExtensions = this._filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.Remote); // Add locally the remote extensions that need to run locally in the web worker for (const ext of remoteExtensionsThatNeedToRunLocally) { @@ -247,10 +248,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } -function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { - return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation); -} - function includes(extensions: IExtensionDescription[], identifier: ExtensionIdentifier): boolean { for (const extension of extensions) { if (ExtensionIdentifier.equals(extension.identifier, identifier)) { diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 8d3a37985b0..5c045163b1b 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -12,11 +12,11 @@ import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementSer import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IURLHandler, IURLService, IOpenURLOptions } from 'vs/platform/url/common/url'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, toExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -148,7 +148,7 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { const extension = await this.extensionService.getExtension(extensionId); if (!extension) { - await this.handleUnhandledURL(uri, { id: extensionId }); + await this.handleUnhandledURL(uri, { id: extensionId }, options); return true; } @@ -232,56 +232,12 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { return await handler.handleURL(uri, options); } - private async handleUnhandledURL(uri: URI, extensionIdentifier: IExtensionIdentifier): Promise { + private async handleUnhandledURL(uri: URI, extensionIdentifier: IExtensionIdentifier, options?: IOpenURLOptions): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); - const extension = installedExtensions.filter(e => areSameExtensions(e.identifier, extensionIdentifier))[0]; - - // Extension is installed - if (extension) { - const enabled = this.extensionEnablementService.isEnabled(extension); - - // Extension is not running. Reload the window to handle. - if (enabled) { - this.telemetryService.publicLog2('uri_invoked/activate_extension/start', { extensionId: extensionIdentifier.id }); - const result = await this.dialogService.confirm({ - message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extension.manifest.displayName || extension.manifest.name), - detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, - primaryButton: localize('reloadAndOpen', "&&Reload Window and Open"), - type: 'question' - }); - - if (!result.confirmed) { - this.telemetryService.publicLog2('uri_invoked/activate_extension/cancel', { extensionId: extensionIdentifier.id }); - return; - } - - this.telemetryService.publicLog2('uri_invoked/activate_extension/accept', { extensionId: extensionIdentifier.id }); - await this.reloadAndHandle(uri); - } - - // Extension is disabled. Enable the extension and reload the window to handle. - else if (this.extensionEnablementService.canChangeEnablement(extension)) { - this.telemetryService.publicLog2('uri_invoked/enable_extension/start', { extensionId: extensionIdentifier.id }); - const result = await this.dialogService.confirm({ - message: localize('enableAndHandle', "Extension '{0}' is disabled. Would you like to enable the extension and reload the window to open the URL?", extension.manifest.displayName || extension.manifest.name), - detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, - primaryButton: localize('enableAndReload', "&&Enable and Open"), - type: 'question' - }); - - if (!result.confirmed) { - this.telemetryService.publicLog2('uri_invoked/enable_extension/cancel', { extensionId: extensionIdentifier.id }); - return; - } - - this.telemetryService.publicLog2('uri_invoked/enable_extension/accept', { extensionId: extensionIdentifier.id }); - await this.extensionEnablementService.setEnablement([extension], EnablementState.EnabledGlobally); - await this.reloadAndHandle(uri); - } - } + let extension = installedExtensions.find(e => areSameExtensions(e.identifier, extensionIdentifier)); // Extension is not installed - else { + if (!extension) { let galleryExtension: IGalleryExtension | undefined; try { @@ -298,9 +254,9 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { // Install the Extension and reload the window to handle. const result = await this.dialogService.confirm({ - message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and reload the window to open this URL?", galleryExtension.displayName || galleryExtension.name), + message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and open this URL?", galleryExtension.displayName || galleryExtension.name), detail: `${galleryExtension.displayName || galleryExtension.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, - primaryButton: localize('install', "&&Install"), + primaryButton: localize('install and open', "&&Install and Open"), type: 'question' }); @@ -312,31 +268,76 @@ class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler { this.telemetryService.publicLog2('uri_invoked/install_extension/accept', { extensionId: extensionIdentifier.id }); try { - await this.progressService.withProgress({ + extension = await this.progressService.withProgress({ location: ProgressLocation.Notification, title: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) }, () => this.extensionManagementService.installFromGallery(galleryExtension!)); - - this.notificationService.prompt( - Severity.Info, - localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString()), - [{ - label: localize('Reload', "Reload Window and Open"), run: async () => { - this.telemetryService.publicLog2('uri_invoked/install_extension/reload', { extensionId: extensionIdentifier.id }); - await this.reloadAndHandle(uri); - } - }], - { sticky: true } - ); } catch (error) { this.notificationService.error(error); + return; } } + + // Extension is installed but not enabled + if (!this.extensionEnablementService.isEnabled(extension)) { + this.telemetryService.publicLog2('uri_invoked/enable_extension/start', { extensionId: extensionIdentifier.id }); + const result = await this.dialogService.confirm({ + message: localize('enableAndHandle', "Extension '{0}' is disabled. Would you like to enable the extension and open the URL?", extension.manifest.displayName || extension.manifest.name), + detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, + primaryButton: localize('enableAndReload', "&&Enable and Open"), + type: 'question' + }); + + if (!result.confirmed) { + this.telemetryService.publicLog2('uri_invoked/enable_extension/cancel', { extensionId: extensionIdentifier.id }); + return; + } + + this.telemetryService.publicLog2('uri_invoked/enable_extension/accept', { extensionId: extensionIdentifier.id }); + await this.extensionEnablementService.setEnablement([extension], EnablementState.EnabledGlobally); + } + + if (this.extensionService.canAddExtension(toExtensionDescription(extension))) { + await this.waitUntilExtensionIsAdded(extensionIdentifier); + await this.handleURL(uri, { ...options, trusted: true }); + } + + /* Extension cannot be added and require window reload */ + else { + this.telemetryService.publicLog2('uri_invoked/activate_extension/start', { extensionId: extensionIdentifier.id }); + const result = await this.dialogService.confirm({ + message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extension.manifest.displayName || extension.manifest.name), + detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`, + primaryButton: localize('reloadAndOpen', "&&Reload Window and Open"), + type: 'question' + }); + + if (!result.confirmed) { + this.telemetryService.publicLog2('uri_invoked/activate_extension/cancel', { extensionId: extensionIdentifier.id }); + return; + } + + this.telemetryService.publicLog2('uri_invoked/activate_extension/accept', { extensionId: extensionIdentifier.id }); + this.storageService.store(URL_TO_HANDLE, JSON.stringify(uri.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + await this.hostService.reload(); + } } - private async reloadAndHandle(url: URI): Promise { - this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE, StorageTarget.MACHINE); - await this.hostService.reload(); + private async waitUntilExtensionIsAdded(extensionId: IExtensionIdentifier): Promise { + if (!(await this.extensionService.getExtension(extensionId.id))) { + await new Promise((c, e) => { + const disposable = this.extensionService.onDidChangeExtensions(async () => { + try { + if (await this.extensionService.getExtension(extensionId.id)) { + disposable.dispose(); + c(); + } + } catch (error) { + e(error); + } + }); + }); + } } // forget about all uris buffered more than 5 minutes ago diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 55ee8c884d5..85bff25d857 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -16,7 +16,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; -import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { joinPath } from 'vs/base/common/resources'; @@ -29,6 +29,8 @@ import { Barrier } from 'vs/base/common/async'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { FileAccess } from 'vs/base/common/network'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { parentOriginHash } from 'vs/workbench/browser/webview'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export interface IWebWorkerExtensionHostInitData { readonly autoStart: boolean; @@ -41,9 +43,9 @@ export interface IWebWorkerExtensionHostDataProvider { export class WebWorkerExtensionHost extends Disposable implements IExtensionHost { - public readonly kind = ExtensionHostKind.LocalWebWorker; public readonly remoteAuthority = null; public readonly lazyStart: boolean; + public readonly extensions = new ExtensionDescriptionRegistry([]); private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; @@ -56,6 +58,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private readonly _extensionHostLogFile: URI; constructor( + public readonly runningLocation: LocalWebWorkerRunningLocation, lazyStart: boolean, private readonly _initDataProvider: IWebWorkerExtensionHostDataProvider, @ITelemetryService private readonly _telemetryService: ITelemetryService, @@ -76,7 +79,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost this._extensionHostLogFile = joinPath(this._extensionHostLogsLocation, `${ExtensionHostLogFileName}.log`); } - private _getWebWorkerExtensionHostIframeSrc(): string { + private async _getWebWorkerExtensionHostIframeSrc(): Promise { const suffix = this._environmentService.debugExtensionHost && this._environmentService.debugRenderer ? '?debugged=1' : '?'; const iframeModulePath = 'vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html'; if (platform.isWeb) { @@ -91,13 +94,18 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost stableOriginUUID = generateUuid(); this._storageService.store(key, stableOriginUUID, StorageScope.WORKSPACE, StorageTarget.MACHINE); } + const hash = await parentOriginHash(window.origin, stableOriginUUID); const baseUrl = ( webEndpointUrlTemplate - .replace('{{uuid}}', stableOriginUUID) + .replace('{{uuid}}', `v--${hash}`) // using `v--` as a marker to require `parentOrigin`/`salt` verification .replace('{{commit}}', commit) .replace('{{quality}}', quality) ); - return `${baseUrl}/out/${iframeModulePath}${suffix}`; + + const res = new URL(`${baseUrl}/out/${iframeModulePath}${suffix}`); + res.searchParams.set('parentOrigin', window.origin); + res.searchParams.set('salt', stableOriginUUID); + return res.toString(); } console.warn(`The web worker extension host is started in a same-origin iframe!`); @@ -109,13 +117,14 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost public async start(): Promise { if (!this._protocolPromise) { - this._protocolPromise = this._startInsideIframe(this._getWebWorkerExtensionHostIframeSrc()); + this._protocolPromise = this._startInsideIframe(); this._protocolPromise.then(protocol => this._protocol = protocol); } return this._protocolPromise; } - private async _startInsideIframe(webWorkerExtensionHostIframeSrc: string): Promise { + private async _startInsideIframe(): Promise { + const webWorkerExtensionHostIframeSrc = await this._getWebWorkerExtensionHostIframeSrc(); const emitter = this._register(new Emitter()); const iframe = document.createElement('iframe'); @@ -258,6 +267,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private async _createExtHostInitData(): Promise { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); + this.extensions.deltaExtensions(initData.extensions, []); return { commit: this._productService.commit, version: this._productService.version, @@ -281,7 +291,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost }, resolvedExtensions: [], hostExtensions: [], - extensions: initData.extensions, + extensions: this.extensions.getAllExtensionDescriptions(), telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._extensionHostLogsLocation, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 1fc62c93ef4..9fffe8eb3d8 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -15,7 +15,7 @@ import { IWebExtensionsScannerService, IWorkbenchExtensionEnablementService } fr import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost, ActivationKind, ExtensionHostKind, toExtensionDescription, ExtensionRunningLocation, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost, ActivationKind, ExtensionHostKind, toExtensionDescription, ExtensionRunningLocation, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, RemoteRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; @@ -133,29 +133,28 @@ export abstract class AbstractExtensionService extends Disposable implements IEx public _serviceBrand: undefined; - protected readonly _onDidRegisterExtensions: Emitter = this._register(new Emitter()); + private readonly _onDidRegisterExtensions: Emitter = this._register(new Emitter()); public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event; - protected readonly _onDidChangeExtensionsStatus: Emitter = this._register(new Emitter()); + private readonly _onDidChangeExtensionsStatus: Emitter = this._register(new Emitter()); public readonly onDidChangeExtensionsStatus: Event = this._onDidChangeExtensionsStatus.event; - protected readonly _onDidChangeExtensions: Emitter = this._register(new Emitter({ leakWarningThreshold: 400 })); + private readonly _onDidChangeExtensions: Emitter = this._register(new Emitter({ leakWarningThreshold: 400 })); public readonly onDidChangeExtensions: Event = this._onDidChangeExtensions.event; - protected readonly _onWillActivateByEvent = this._register(new Emitter()); + private readonly _onWillActivateByEvent = this._register(new Emitter()); public readonly onWillActivateByEvent: Event = this._onWillActivateByEvent.event; - protected readonly _onDidChangeResponsiveChange = this._register(new Emitter()); + private readonly _onDidChangeResponsiveChange = this._register(new Emitter()); public readonly onDidChangeResponsiveChange: Event = this._onDidChangeResponsiveChange.event; - protected readonly _runningLocationClassifier: ExtensionRunningLocationClassifier; protected readonly _registry: ExtensionDescriptionRegistry; private readonly _registryLock: Lock; private readonly _installedExtensionsReady: Barrier; - protected readonly _isDev: boolean; + private readonly _isDev: boolean; private readonly _extensionsMessages: Map; - protected readonly _allRequestedActivateEvents = new Set(); + private readonly _allRequestedActivateEvents = new Set(); private readonly _proposedApiController: ProposedApiController; private readonly _isExtensionDevHost: boolean; protected readonly _isExtensionDevTestFromCli: boolean; @@ -163,10 +162,12 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _deltaExtensionsQueue: DeltaExtensionsQueueItem[]; private _inHandleDeltaExtensions: boolean; - protected _runningLocation: Map; + private _runningLocation: Map; + private _lastExtensionHostId: number = 0; + private _maxLocalProcessAffinity: number = 0; // --- Members used per extension host process - protected _extensionHostManagers: IExtensionHostManager[]; + private _extensionHostManagers: IExtensionHostManager[]; protected _extensionHostActiveExtensions: Map; private _extensionHostActivationTimes: Map; private _extensionHostExtensionRuntimeErrors: Map; @@ -189,11 +190,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx ) { super(); - this._runningLocationClassifier = new ExtensionRunningLocationClassifier( - (extension) => this._getExtensionKind(extension), - (extensionId, extensionKinds, isInstalledLocally, isInstalledRemotely, preference) => this._pickRunningLocation(extensionId, extensionKinds, isInstalledLocally, isInstalledRemotely, preference) - ); - // help the file service to activate providers by activating extensions by file system event this._register(this._fileService.onWillActivateFileSystemProvider(e => { if (e.scheme !== Schemas.vscodeRemote) { @@ -266,17 +262,251 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return this._extensionManifestPropertiesService.getExtensionKind(extensionDescription); } - protected abstract _pickRunningLocation(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionRunningLocation; + protected abstract _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null; - protected _getExtensionHostManager(kind: ExtensionHostKind): IExtensionHostManager | null { + protected _getExtensionHostManagers(kind: ExtensionHostKind): IExtensionHostManager[] { + return this._extensionHostManagers.filter(extHostManager => extHostManager.kind === kind); + } + + protected _getExtensionHostManagerByRunningLocation(runningLocation: ExtensionRunningLocation): IExtensionHostManager | null { for (const extensionHostManager of this._extensionHostManagers) { - if (extensionHostManager.kind === kind) { + if (extensionHostManager.representsRunningLocation(runningLocation)) { return extensionHostManager; } } return null; } + //#region running location + + private _computeAffinity(inputExtensions: IExtensionDescription[], extensionHostKind: ExtensionHostKind, isInitialAllocation: boolean): { affinities: Map; maxAffinity: number } { + // Only analyze extensions that can execute + const extensions = new Map(); + for (const extension of inputExtensions) { + if (extension.main || extension.browser) { + extensions.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + } + // Also add existing extensions of the same kind that can execute + for (const extension of this._registry.getAllExtensionDescriptions()) { + if (extension.main || extension.browser) { + const runningLocation = this._runningLocation.get(ExtensionIdentifier.toKey(extension.identifier)); + if (runningLocation && runningLocation.kind === extensionHostKind) { + extensions.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + } + } + + // Initially, each extension belongs to its own group + const groups = new Map(); + let groupNumber = 0; + for (const [_, extension] of extensions) { + groups.set(ExtensionIdentifier.toKey(extension.identifier), ++groupNumber); + } + + const changeGroup = (from: number, to: number) => { + for (const [key, group] of groups) { + if (group === from) { + groups.set(key, to); + } + } + }; + + // We will group things together when there are dependencies + for (const [_, extension] of extensions) { + if (!extension.extensionDependencies) { + continue; + } + const myGroup = groups.get(ExtensionIdentifier.toKey(extension.identifier))!; + for (const depId of extension.extensionDependencies) { + const depGroup = groups.get(ExtensionIdentifier.toKey(depId)); + if (!depGroup) { + // probably can't execute, so it has no impact + continue; + } + + if (depGroup === myGroup) { + // already in the same group + continue; + } + + changeGroup(depGroup, myGroup); + } + } + + // Initialize with existing affinities + const resultingAffinities = new Map(); + let lastAffinity = 0; + for (const [_, extension] of extensions) { + const runningLocation = this._runningLocation.get(ExtensionIdentifier.toKey(extension.identifier)); + if (runningLocation) { + const group = groups.get(ExtensionIdentifier.toKey(extension.identifier))!; + resultingAffinities.set(group, runningLocation.affinity); + lastAffinity = Math.max(lastAffinity, runningLocation.affinity); + } + } + + // When doing extension host debugging, we will ignore the configured affinity + // because we can currently debug a single extension host + if (!this._environmentService.isExtensionDevelopment) { + // Go through each configured affinity and try to accomodate it + const configuredAffinities = this._configurationService.getValue<{ [extensionId: string]: number } | undefined>('extensions.experimental.affinity') || {}; + const configuredExtensionIds = Object.keys(configuredAffinities); + const configuredAffinityToResultingAffinity = new Map(); + for (const extensionId of configuredExtensionIds) { + const configuredAffinity = configuredAffinities[extensionId]; + if (typeof configuredAffinity !== 'number' || configuredAffinity <= 0 || Math.floor(configuredAffinity) !== configuredAffinity) { + this._logService.info(`Ignoring configured affinity for '${extensionId}' because the value is not a positive integer.`); + continue; + } + const group = groups.get(ExtensionIdentifier.toKey(extensionId)); + if (!group) { + this._logService.info(`Ignoring configured affinity for '${extensionId}' because the extension is unknown or cannot execute.`); + continue; + } + + const affinity1 = resultingAffinities.get(group); + if (affinity1) { + // Affinity for this group is already established + configuredAffinityToResultingAffinity.set(configuredAffinity, affinity1); + continue; + } + + const affinity2 = configuredAffinityToResultingAffinity.get(configuredAffinity); + if (affinity2) { + // Affinity for this configuration is already established + resultingAffinities.set(group, affinity2); + continue; + } + + if (!isInitialAllocation) { + this._logService.info(`Ignoring configured affinity for '${extensionId}' because extension host(s) are already running. Reload window.`); + continue; + } + + const affinity3 = ++lastAffinity; + configuredAffinityToResultingAffinity.set(configuredAffinity, affinity3); + resultingAffinities.set(group, affinity3); + } + } + + const result = new Map(); + for (const extension of inputExtensions) { + const group = groups.get(ExtensionIdentifier.toKey(extension.identifier)) || 0; + const affinity = resultingAffinities.get(group) || 0; + result.set(ExtensionIdentifier.toKey(extension.identifier), affinity); + } + + if (lastAffinity > 0 && isInitialAllocation) { + for (let affinity = 1; affinity <= lastAffinity; affinity++) { + const extensionIds: ExtensionIdentifier[] = []; + for (const extension of inputExtensions) { + if (result.get(ExtensionIdentifier.toKey(extension.identifier)) === affinity) { + extensionIds.push(extension.identifier); + } + } + this._logService.info(`Placing extension(s) ${extensionIds.map(e => e.value).join(', ')} on a separate extension host.`); + } + } + + return { affinities: result, maxAffinity: lastAffinity }; + } + + private _computeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[], isInitialAllocation: boolean): { runningLocation: Map; maxLocalProcessAffinity: number } { + const extensionHostKinds = ExtensionHostKindClassifier.determineExtensionHostKinds( + localExtensions, + remoteExtensions, + (extension) => this._getExtensionKind(extension), + (extensionId, extensionKinds, isInstalledLocally, isInstalledRemotely, preference) => this._pickExtensionHostKind(extensionId, extensionKinds, isInstalledLocally, isInstalledRemotely, preference) + ); + + const extensions = new Map(); + for (const extension of localExtensions) { + extensions.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + for (const extension of remoteExtensions) { + extensions.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + + const result = new Map(); + const localProcessExtensions: IExtensionDescription[] = []; + for (const [extensionIdKey, extensionHostKind] of extensionHostKinds) { + let runningLocation: ExtensionRunningLocation | null = null; + if (extensionHostKind === ExtensionHostKind.LocalProcess) { + const extensionDescription = extensions.get(ExtensionIdentifier.toKey(extensionIdKey)); + if (extensionDescription) { + localProcessExtensions.push(extensionDescription); + } + } else if (extensionHostKind === ExtensionHostKind.LocalWebWorker) { + runningLocation = new LocalWebWorkerRunningLocation(); + } else if (extensionHostKind === ExtensionHostKind.Remote) { + runningLocation = new RemoteRunningLocation(); + } + result.set(extensionIdKey, runningLocation); + } + + const { affinities, maxAffinity } = this._computeAffinity(localProcessExtensions, ExtensionHostKind.LocalProcess, isInitialAllocation); + for (const extension of localProcessExtensions) { + const affinity = affinities.get(ExtensionIdentifier.toKey(extension.identifier)) || 0; + result.set(ExtensionIdentifier.toKey(extension.identifier), new LocalProcessRunningLocation(affinity)); + } + + return { runningLocation: result, maxLocalProcessAffinity: maxAffinity }; + } + + protected _determineRunningLocation(localExtensions: IExtensionDescription[]): Map { + return this._computeRunningLocation(localExtensions, [], false).runningLocation; + } + + protected _initializeRunningLocation(localExtensions: IExtensionDescription[], remoteExtensions: IExtensionDescription[]): void { + const { runningLocation, maxLocalProcessAffinity } = this._computeRunningLocation(localExtensions, remoteExtensions, true); + this._runningLocation = runningLocation; + this._maxLocalProcessAffinity = maxLocalProcessAffinity; + this._startExtensionHostsIfNecessary(true, []); + } + + /** + * Update `this._runningLocation` with running locations for newly enabled/installed extensions. + */ + private _updateRunningLocationForAddedExtensions(toAdd: IExtensionDescription[]): void { + // Determine new running location + const localProcessExtensions: IExtensionDescription[] = []; + for (const extension of toAdd) { + const extensionKind = this._getExtensionKind(extension); + const isRemote = extension.extensionLocation.scheme === Schemas.vscodeRemote; + const extensionHostKind = this._pickExtensionHostKind(extension.identifier, extensionKind, !isRemote, isRemote, ExtensionRunningPreference.None); + let runningLocation: ExtensionRunningLocation | null = null; + if (extensionHostKind === ExtensionHostKind.LocalProcess) { + localProcessExtensions.push(extension); + } else if (extensionHostKind === ExtensionHostKind.LocalWebWorker) { + runningLocation = new LocalWebWorkerRunningLocation(); + } else if (extensionHostKind === ExtensionHostKind.Remote) { + runningLocation = new RemoteRunningLocation(); + } + this._runningLocation.set(ExtensionIdentifier.toKey(extension.identifier), runningLocation); + } + + const { affinities } = this._computeAffinity(localProcessExtensions, ExtensionHostKind.LocalProcess, false); + for (const extension of localProcessExtensions) { + const affinity = affinities.get(ExtensionIdentifier.toKey(extension.identifier)) || 0; + this._runningLocation.set(ExtensionIdentifier.toKey(extension.identifier), new LocalProcessRunningLocation(affinity)); + } + } + + protected _filterByRunningLocation(extensions: IExtensionDescription[], desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { + return filterByRunningLocation(extensions, this._runningLocation, desiredRunningLocation); + } + + protected _filterByExtensionHostKind(extensions: IExtensionDescription[], desiredExtensionHostKind: ExtensionHostKind): IExtensionDescription[] { + return filterByExtensionHostKind(extensions, this._runningLocation, desiredExtensionHostKind); + } + + protected _filterByExtensionHostManager(extensions: IExtensionDescription[], extensionHostManager: IExtensionHostManager): IExtensionDescription[] { + return filterByExtensionHostManager(extensions, this._runningLocation, extensionHostManager); + } + + //#endregion + //#region deltaExtensions private async _handleDeltaExtensions(item: DeltaExtensionsQueueItem): Promise { @@ -376,47 +606,32 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } private async _updateExtensionsOnExtHosts(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { - const groupedToRemove: ExtensionIdentifier[][] = []; - const groupRemove = (extensionHostKind: ExtensionHostKind, extensionRunningLocation: ExtensionRunningLocation) => { - groupedToRemove[extensionHostKind] = filterByRunningLocation(toRemove, extId => extId, this._runningLocation, extensionRunningLocation); - }; - groupRemove(ExtensionHostKind.LocalProcess, ExtensionRunningLocation.LocalProcess); - groupRemove(ExtensionHostKind.LocalWebWorker, ExtensionRunningLocation.LocalWebWorker); - groupRemove(ExtensionHostKind.Remote, ExtensionRunningLocation.Remote); + + // Remove old running location + const removedRunningLocation = new Map(); for (const extensionId of toRemove) { - this._runningLocation.delete(ExtensionIdentifier.toKey(extensionId)); + const extensionKey = ExtensionIdentifier.toKey(extensionId); + removedRunningLocation.set(extensionKey, this._runningLocation.get(extensionKey) || null); + this._runningLocation.delete(extensionKey); } - const groupedToAdd: IExtensionDescription[][] = []; - const groupAdd = (extensionHostKind: ExtensionHostKind, extensionRunningLocation: ExtensionRunningLocation) => { - groupedToAdd[extensionHostKind] = filterByRunningLocation(toAdd, ext => ext.identifier, this._runningLocation, extensionRunningLocation); - }; - for (const extension of toAdd) { - const extensionKind = this._getExtensionKind(extension); - const isRemote = extension.extensionLocation.scheme === Schemas.vscodeRemote; - const runningLocation = this._pickRunningLocation(extension.identifier, extensionKind, !isRemote, isRemote, ExtensionRunningPreference.None); - this._runningLocation.set(ExtensionIdentifier.toKey(extension.identifier), runningLocation); - } - groupAdd(ExtensionHostKind.LocalProcess, ExtensionRunningLocation.LocalProcess); - groupAdd(ExtensionHostKind.LocalWebWorker, ExtensionRunningLocation.LocalWebWorker); - groupAdd(ExtensionHostKind.Remote, ExtensionRunningLocation.Remote); - - const promises: Promise[] = []; - - for (const extensionHostKind of [ExtensionHostKind.LocalProcess, ExtensionHostKind.LocalWebWorker, ExtensionHostKind.Remote]) { - const toAdd = groupedToAdd[extensionHostKind]; - const toRemove = groupedToRemove[extensionHostKind]; - if (toAdd.length > 0 || toRemove.length > 0) { - const extensionHostManager = this._getExtensionHostManager(extensionHostKind); - if (extensionHostManager) { - promises.push(extensionHostManager.deltaExtensions(toAdd, toRemove)); - } - } - } + // Determine new running location + this._updateRunningLocationForAddedExtensions(toAdd); + const promises = this._extensionHostManagers.map( + extHostManager => this._updateExtensionsOnExtHost(extHostManager, toAdd, toRemove, removedRunningLocation) + ); await Promise.all(promises); } + private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, _toAdd: IExtensionDescription[], _toRemove: ExtensionIdentifier[], removedRunningLocation: Map): Promise { + const toAdd = filterByExtensionHostManager(_toAdd, this._runningLocation, extensionHostManager); + const toRemove = _filterByExtensionHostManager(_toRemove, extId => extId, removedRunningLocation, extensionHostManager); + if (toRemove.length > 0 || toAdd.length > 0) { + await extensionHostManager.deltaExtensions(toAdd, toRemove); + } + } + public canAddExtension(extension: IExtensionDescription): boolean { const existing = this._registry.getExtensionDescription(extension.identifier); if (existing) { @@ -431,8 +646,8 @@ export abstract class AbstractExtensionService extends Disposable implements IEx const extensionKind = this._getExtensionKind(extension); const isRemote = extension.extensionLocation.scheme === Schemas.vscodeRemote; - const runningLocation = this._pickRunningLocation(extension.identifier, extensionKind, !isRemote, isRemote, ExtensionRunningPreference.None); - if (runningLocation === ExtensionRunningLocation.None) { + const extensionHostKind = this._pickExtensionHostKind(extension.identifier, extensionKind, !isRemote, isRemote, ExtensionRunningPreference.None); + if (extensionHostKind === null) { return false; } @@ -520,7 +735,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx protected async _initialize(): Promise { perf.mark('code/willLoadExtensions'); - this._startExtensionHosts(true, []); + this._startExtensionHostsIfNecessary(true, []); const lock = await this._registryLock.acquire('_initialize'); try { @@ -560,38 +775,31 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._onExtensionHostExit(exitCode); } - private findTestExtensionHost(testLocation: URI): IExtensionHostManager | undefined | null { - let extensionHostKind: ExtensionHostKind | undefined; + private findTestExtensionHost(testLocation: URI): IExtensionHostManager | null { + let runningLocation: ExtensionRunningLocation | null = null; for (const extension of this._registry.getAllExtensionDescriptions()) { if (isEqualOrParent(testLocation, extension.extensionLocation)) { - const runningLocation = this._runningLocation.get(ExtensionIdentifier.toKey(extension.identifier)); - if (runningLocation === ExtensionRunningLocation.LocalProcess) { - extensionHostKind = ExtensionHostKind.LocalProcess; - } else if (runningLocation === ExtensionRunningLocation.LocalWebWorker) { - extensionHostKind = ExtensionHostKind.LocalWebWorker; - } else if (runningLocation === ExtensionRunningLocation.Remote) { - extensionHostKind = ExtensionHostKind.Remote; - } + runningLocation = this._runningLocation.get(ExtensionIdentifier.toKey(extension.identifier)) || null; break; } } - if (extensionHostKind === undefined) { + if (runningLocation === null) { // not sure if we should support that, but it was possible to have an test outside an extension if (testLocation.scheme === Schemas.vscodeRemote) { - extensionHostKind = ExtensionHostKind.Remote; + runningLocation = new RemoteRunningLocation(); } else { // When a debugger attaches to the extension host, it will surface all console.log messages from the extension host, // but not necessarily from the window. So it would be best if any errors get printed to the console of the extension host. // That is why here we use the local process extension host even for non-file URIs - extensionHostKind = ExtensionHostKind.LocalProcess; + runningLocation = new LocalProcessRunningLocation(0); } } - if (extensionHostKind !== undefined) { - return this._getExtensionHostManager(extensionHostKind); + if (runningLocation !== null) { + return this._getExtensionHostManagerByRunningLocation(runningLocation); } - return undefined; + return null; } private _releaseBarrier(): void { @@ -621,14 +829,42 @@ export abstract class AbstractExtensionService extends Disposable implements IEx } } - private _startExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): void { - const extensionHosts = this._createExtensionHosts(isInitialStart); - extensionHosts.forEach((extensionHost) => { - const processManager: IExtensionHostManager = createExtensionHostManager(this._instantiationService, extensionHost, isInitialStart, initialActivationEvents, this._acquireInternalAPI()); - processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal)); - processManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); }); - this._extensionHostManagers.push(processManager); + private _startExtensionHostsIfNecessary(isInitialStart: boolean, initialActivationEvents: string[]): void { + const locations: ExtensionRunningLocation[] = []; + for (let affinity = 0; affinity <= this._maxLocalProcessAffinity; affinity++) { + locations.push(new LocalProcessRunningLocation(affinity)); + } + locations.push(new LocalWebWorkerRunningLocation()); + locations.push(new RemoteRunningLocation()); + for (const location of locations) { + if (this._getExtensionHostManagerByRunningLocation(location)) { + // already running + continue; + } + const extHostManager = this._createExtensionHostManager(location, isInitialStart, initialActivationEvents); + if (extHostManager) { + this._extensionHostManagers.push(extHostManager); + } + } + } + + private _createExtensionHostManager(runningLocation: ExtensionRunningLocation, isInitialStart: boolean, initialActivationEvents: string[]): IExtensionHostManager | null { + const extensionHost = this._createExtensionHost(runningLocation, isInitialStart); + if (!extensionHost) { + return null; + } + + const extensionHostId = String(++this._lastExtensionHostId); + const processManager: IExtensionHostManager = createExtensionHostManager(this._instantiationService, extensionHostId, extensionHost, isInitialStart, initialActivationEvents, this._acquireInternalAPI()); + processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal)); + processManager.onDidChangeResponsiveState((responsiveState) => { + this._onDidChangeResponsiveChange.fire({ + extensionHostId: extensionHostId, + extensionHostKind: processManager.kind, + isResponsive: responsiveState === ResponsiveState.Responsive + }); }); + return processManager; } private _onExtensionHostCrashOrExit(extensionHost: IExtensionHostManager, code: number, signal: string | null): void { @@ -697,12 +933,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx const lock = await this._registryLock.acquire('startExtensionHosts'); try { - this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); + this._startExtensionHostsIfNecessary(false, Array.from(this._allRequestedActivateEvents.keys())); - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); - if (localProcessExtensionHost) { - await localProcessExtensionHost.ready(); - } + const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); + await Promise.all(localProcessExtensionHosts.map(extHost => extHost.ready())); } finally { lock.dispose(); } @@ -808,15 +1042,28 @@ export abstract class AbstractExtensionService extends Disposable implements IEx messages: this._extensionsMessages.get(extensionKey) || [], activationTimes: this._extensionHostActivationTimes.get(extensionKey), runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey) || [], - runningLocation: this._runningLocation.get(extensionKey) || ExtensionRunningLocation.None, + runningLocation: this._runningLocation.get(extensionKey) || null, }; } } return result; } - public getInspectPort(_tryEnableInspector: boolean): Promise { - return Promise.resolve(0); + public async getInspectPort(extensionHostId: string, tryEnableInspector: boolean): Promise { + for (const extHostManager of this._extensionHostManagers) { + if (extHostManager.extensionHostId === extensionHostId) { + return extHostManager.getInspectPort(tryEnableInspector); + } + } + return 0; + } + + public async getInspectPorts(extensionHostKind: ExtensionHostKind, tryEnableInspector: boolean): Promise { + const result = await Promise.all( + this._getExtensionHostManagers(extensionHostKind).map(extHost => extHost.getInspectPort(tryEnableInspector)) + ); + // remove 0s: + return result.filter(element => Boolean(element)); } public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise { @@ -1077,7 +1324,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx //#endregion - protected abstract _createExtensionHosts(isInitialStart: boolean): IExtensionHost[]; + protected abstract _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null; protected abstract _scanAndHandleExtensions(): Promise; protected abstract _scanSingleExtension(extension: IExtension): Promise; public abstract _onExtensionHostExit(code: number): void; @@ -1131,25 +1378,28 @@ class ExtensionInfo { } } -class ExtensionRunningLocationClassifier { - constructor( - private readonly getExtensionKind: (extensionDescription: IExtensionDescription) => ExtensionKind[], - private readonly pickRunningLocation: (extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference) => ExtensionRunningLocation, - ) { - } +class ExtensionHostKindClassifier { - private _toExtensionWithKind(extensions: IExtensionDescription[]): Map { + private static _toExtensionWithKind( + extensions: IExtensionDescription[], + getExtensionKind: (extensionDescription: IExtensionDescription) => ExtensionKind[] + ): Map { const result = new Map(); extensions.forEach((desc) => { - const ext = new ExtensionWithKind(desc, this.getExtensionKind(desc)); + const ext = new ExtensionWithKind(desc, getExtensionKind(desc)); result.set(ext.key, ext); }); return result; } - public determineRunningLocation(_localExtensions: IExtensionDescription[], _remoteExtensions: IExtensionDescription[]): Map { - const localExtensions = this._toExtensionWithKind(_localExtensions); - const remoteExtensions = this._toExtensionWithKind(_remoteExtensions); + public static determineExtensionHostKinds( + _localExtensions: IExtensionDescription[], + _remoteExtensions: IExtensionDescription[], + getExtensionKind: (extensionDescription: IExtensionDescription) => ExtensionKind[], + pickExtensionHostKind: (extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference) => ExtensionHostKind | null + ): Map { + const localExtensions = this._toExtensionWithKind(_localExtensions, getExtensionKind); + const remoteExtensions = this._toExtensionWithKind(_remoteExtensions, getExtensionKind); const allExtensions = new Map(); const collectExtension = (ext: ExtensionWithKind) => { @@ -1164,7 +1414,7 @@ class ExtensionRunningLocationClassifier { localExtensions.forEach((ext) => collectExtension(ext)); remoteExtensions.forEach((ext) => collectExtension(ext)); - const runningLocation = new Map(); + const extensionHostKinds = new Map(); allExtensions.forEach((ext) => { const isInstalledLocally = Boolean(ext.local); const isInstalledRemotely = Boolean(ext.remote); @@ -1179,10 +1429,10 @@ class ExtensionRunningLocationClassifier { preference = ExtensionRunningPreference.Remote; } - runningLocation.set(ext.key, this.pickRunningLocation(ext.identifier, ext.kind, isInstalledLocally, isInstalledRemotely, preference)); + extensionHostKinds.set(ext.key, pickExtensionHostKind(ext.identifier, ext.kind, isInstalledLocally, isInstalledRemotely, preference)); }); - return runningLocation; + return extensionHostKinds; } } @@ -1207,14 +1457,6 @@ class ProposedApiController { this._productEnabledExtensions = new Map(); - // todo@jrieken this is deprecated and will be removed - // OLD world - extensions that are listed in `extensionAllowedProposedApi` get all proposals enabled - if (isNonEmptyArray(productService.extensionAllowedProposedApi)) { - for (let id of productService.extensionAllowedProposedApi) { - const key = ExtensionIdentifier.toKey(id); - this._productEnabledExtensions.set(key, Object.keys(allApiProposals)); - } - } // NEW world - product.json spells out what proposals each extension can use if (productService.extensionEnabledApiProposals) { @@ -1227,9 +1469,6 @@ class ProposedApiController { } return true; }); - if (this._productEnabledExtensions.has(key)) { - _logService.warn(`Extension '${key}' appears in BOTH 'product.json#extensionAllowedProposedApi' and 'extensionEnabledApiProposals'. The latter is more restrictive and will override the former.`); - } this._productEnabledExtensions.set(key, proposalNames); }); } @@ -1286,12 +1525,35 @@ class ProposedApiController { if (!extension.isBuiltin && isNonEmptyArray(extension.enabledApiProposals)) { // restrictive: extension cannot use proposed API in this context and its declaration is nulled - this._logService.critical(`Extension '${extension.identifier.value} CANNOT USE these API proposals '${extension.enabledApiProposals?.join(', ') ?? '*'}'. You MUST start in extension development mode or use the --enable-proposed-api command line flag`); + this._logService.critical(`Extension '${extension.identifier.value} CANNOT USE these API proposals '${extension.enabledApiProposals?.join(', ') || '*'}'. You MUST start in extension development mode or use the --enable-proposed-api command line flag`); extension.enabledApiProposals = []; } } } -function filterByRunningLocation(extensions: T[], extId: (item: T) => ExtensionIdentifier, runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): T[] { - return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(extId(ext))) === desiredRunningLocation); +export function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { + return _filterByRunningLocation(extensions, ext => ext.identifier, runningLocation, desiredRunningLocation); +} + +function _filterByRunningLocation(extensions: T[], extId: (item: T) => ExtensionIdentifier, runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): T[] { + return _filterExtensions(extensions, extId, runningLocation, extRunningLocation => desiredRunningLocation.equals(extRunningLocation)); +} + +function filterByExtensionHostKind(extensions: IExtensionDescription[], runningLocation: Map, desiredExtensionHostKind: ExtensionHostKind): IExtensionDescription[] { + return _filterExtensions(extensions, ext => ext.identifier, runningLocation, extRunningLocation => extRunningLocation.kind === desiredExtensionHostKind); +} + +function filterByExtensionHostManager(extensions: IExtensionDescription[], runningLocation: Map, extensionHostManager: IExtensionHostManager): IExtensionDescription[] { + return _filterByExtensionHostManager(extensions, ext => ext.identifier, runningLocation, extensionHostManager); +} + +function _filterByExtensionHostManager(extensions: T[], extId: (item: T) => ExtensionIdentifier, runningLocation: Map, extensionHostManager: IExtensionHostManager): T[] { + return _filterExtensions(extensions, extId, runningLocation, extRunningLocation => extensionHostManager.representsRunningLocation(extRunningLocation)); +} + +function _filterExtensions(extensions: T[], extId: (item: T) => ExtensionIdentifier, runningLocation: Map, predicate: (extRunningLocation: ExtensionRunningLocation) => boolean): T[] { + return extensions.filter((ext) => { + const extRunningLocation = runningLocation.get(ExtensionIdentifier.toKey(extId(ext))); + return extRunningLocation && predicate(extRunningLocation); + }); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 72400588d16..c809d5a4b5a 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -12,49 +12,54 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ExtHostCustomersRegistry, IInternalExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { Proxied, ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as nls from 'vs/nls'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Barrier, timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IExtensionHostProxy } from 'vs/workbench/services/extensions/common/extensionHostProxy'; +import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; const LOG_USE_COLORS = true; export interface IExtensionHostManager { + readonly extensionHostId: string; readonly kind: ExtensionHostKind; readonly onDidExit: Event<[number, string | null]>; readonly onDidChangeResponsiveState: Event; dispose(): void; ready(): Promise; + representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean; deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; activationEventIsDone(activationEvent: string): boolean; getInspectPort(tryEnableInspector: boolean): Promise; - resolveAuthority(remoteAuthority: string): Promise; - getCanonicalURI(remoteAuthority: string, uri: URI): Promise; + resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; + /** + * Returns `null` if no resolver for `remoteAuthority` is found. + */ + getCanonicalURI(remoteAuthority: string, uri: URI): Promise; start(enabledExtensionIds: ExtensionIdentifier[]): Promise; extensionTestsExecute(): Promise; extensionTestsSendExit(exitCode: number): Promise; setRemoteEnvironment(env: { [key: string]: string | null }): Promise; } -export function createExtensionHostManager(instantiationService: IInstantiationService, extensionHost: IExtensionHost, isInitialStart: boolean, initialActivationEvents: string[], internalExtensionService: IInternalExtensionService): IExtensionHostManager { +export function createExtensionHostManager(instantiationService: IInstantiationService, extensionHostId: string, extensionHost: IExtensionHost, isInitialStart: boolean, initialActivationEvents: string[], internalExtensionService: IInternalExtensionService): IExtensionHostManager { if (extensionHost.lazyStart && isInitialStart && initialActivationEvents.length === 0) { - return instantiationService.createInstance(LazyStartExtensionHostManager, extensionHost, internalExtensionService); + return instantiationService.createInstance(LazyStartExtensionHostManager, extensionHostId, extensionHost, internalExtensionService); } - return instantiationService.createInstance(ExtensionHostManager, extensionHost, initialActivationEvents, internalExtensionService); + return instantiationService.createInstance(ExtensionHostManager, extensionHostId, extensionHost, initialActivationEvents, internalExtensionService); } export type ExtensionHostStartupClassification = { @@ -77,7 +82,6 @@ export type ExtensionHostStartupEvent = { class ExtensionHostManager extends Disposable implements IExtensionHostManager { - public readonly kind: ExtensionHostKind; public readonly onDidExit: Event<[number, string | null]>; private readonly _onDidChangeResponsiveState: Emitter = this._register(new Emitter()); @@ -92,10 +96,14 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { private readonly _customers: IDisposable[]; private readonly _extensionHost: IExtensionHost; private _proxy: Promise | null; - private _resolveAuthorityAttempt: number; private _hasStarted = false; + public get kind(): ExtensionHostKind { + return this._extensionHost.runningLocation.kind; + } + constructor( + public readonly extensionHostId: string, extensionHost: IExtensionHost, initialActivationEvents: string[], private readonly _internalExtensionService: IInternalExtensionService, @@ -111,7 +119,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { this._customers = []; this._extensionHost = extensionHost; - this.kind = this._extensionHost.kind; this.onDidExit = this._extensionHost.onExit; const startingTelemetryEvent: ExtensionHostStartupEvent = { @@ -166,7 +173,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { measure: () => this.measure() })); }); - this._resolveAuthorityAttempt = 0; } public override dispose(): void { @@ -190,7 +196,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } private async measure(): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return null; } @@ -205,12 +211,8 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { }; } - private async _getProxy(): Promise { - return this._proxy; - } - public async ready(): Promise { - await this._getProxy(); + await this._proxy; } private async _measureLatency(proxy: IExtensionHostProxy): Promise { @@ -310,7 +312,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } public async activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return false; } @@ -359,57 +361,52 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return 0; } - public async resolveAuthority(remoteAuthority: string): Promise { - const authorityPlusIndex = remoteAuthority.indexOf('+'); - if (authorityPlusIndex === -1) { - // This authority does not need to be resolved, simply parse the port number - const lastColon = remoteAuthority.lastIndexOf(':'); - return Promise.resolve({ - authority: { - authority: remoteAuthority, - host: remoteAuthority.substring(0, lastColon), - port: parseInt(remoteAuthority.substring(lastColon + 1), 10), - connectionToken: undefined - } - }); - } - const proxy = await this._getProxy(); + public async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { + const proxy = await this._proxy; if (!proxy) { - throw new Error(`Cannot resolve authority`); + return { + type: 'error', + error: { + message: `Cannot resolve authority`, + code: RemoteAuthorityResolverErrorCode.Unknown, + detail: undefined + } + }; } - this._resolveAuthorityAttempt++; - const result = await proxy.resolveAuthority(remoteAuthority, this._resolveAuthorityAttempt); - if (result.type === 'ok') { - return result.value; - } else { - throw new RemoteAuthorityResolverError(result.error.message, result.error.code, result.error.detail); + + try { + return proxy.resolveAuthority(remoteAuthority, resolveAttempt); + } catch (err) { + return { + type: 'error', + error: { + message: err.message, + code: RemoteAuthorityResolverErrorCode.Unknown, + detail: err + } + }; } } - public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { - const authorityPlusIndex = remoteAuthority.indexOf('+'); - if (authorityPlusIndex === -1) { - // This authority does not use a resolver - return uri; - } - const proxy = await this._getProxy(); + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + const proxy = await this._proxy; if (!proxy) { throw new Error(`Cannot resolve canonical URI`); } - const result = await proxy.getCanonicalURI(remoteAuthority, uri); - return URI.revive(result); + return proxy.getCanonicalURI(remoteAuthority, uri); } public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return; } + this._extensionHost.extensions.keepOnly(enabledExtensionIds); return proxy.startExtensionHost(enabledExtensionIds); } public async extensionTestsExecute(): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { throw new Error('Could not obtain Extension Host Proxy'); } @@ -417,7 +414,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } public async extensionTestsSendExit(exitCode: number): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return; } @@ -430,16 +427,21 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { } } + public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { + return this._extensionHost.runningLocation.equals(runningLocation); + } + public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return; } + this._extensionHost.extensions.deltaExtensions(toAdd, toRemove); return proxy.deltaExtensions(toAdd, toRemove); } public async setRemoteEnvironment(env: { [key: string]: string | null }): Promise { - const proxy = await this._getProxy(); + const proxy = await this._proxy; if (!proxy) { return; } @@ -452,7 +454,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { * Waits until `start()` and only if it has extensions proceeds to really start. */ class LazyStartExtensionHostManager extends Disposable implements IExtensionHostManager { - public readonly kind: ExtensionHostKind; + public readonly onDidExit: Event<[number, string | null]>; private readonly _onDidChangeResponsiveState: Emitter = this._register(new Emitter()); public readonly onDidChangeResponsiveState: Event = this._onDidChangeResponsiveState.event; @@ -461,7 +463,12 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost private _startCalled: Barrier; private _actual: ExtensionHostManager | null; + public get kind(): ExtensionHostKind { + return this._extensionHost.runningLocation.kind; + } + constructor( + public readonly extensionHostId: string, extensionHost: IExtensionHost, private readonly _internalExtensionService: IInternalExtensionService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -469,7 +476,6 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost ) { super(); this._extensionHost = extensionHost; - this.kind = extensionHost.kind; this.onDidExit = extensionHost.onExit; this._startCalled = new Barrier(); this._actual = null; @@ -477,7 +483,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost private _createActual(reason: string): ExtensionHostManager { this._logService.info(`Creating lazy extension host: ${reason}`); - this._actual = this._register(this._instantiationService.createInstance(ExtensionHostManager, this._extensionHost, [], this._internalExtensionService)); + this._actual = this._register(this._instantiationService.createInstance(ExtensionHostManager, this.extensionHostId, this._extensionHost, [], this._internalExtensionService)); this._register(this._actual.onDidChangeResponsiveState((e) => this._onDidChangeResponsiveState.fire(e))); return this._actual; } @@ -498,6 +504,9 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost await this._actual.ready(); } } + public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { + return this._extensionHost.runningLocation.equals(runningLocation); + } public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { await this._startCalled.wait(); const extensionHostAlreadyStarted = Boolean(this._actual); @@ -543,14 +552,21 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost } return 0; } - public async resolveAuthority(remoteAuthority: string): Promise { + public async resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise { await this._startCalled.wait(); if (this._actual) { - return this._actual.resolveAuthority(remoteAuthority); + return this._actual.resolveAuthority(remoteAuthority, resolveAttempt); } - throw new Error(`Cannot resolve authority`); + return { + type: 'error', + error: { + message: `Cannot resolve authority`, + code: RemoteAuthorityResolverErrorCode.Unknown, + detail: undefined + } + }; } - public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + public async getCanonicalURI(remoteAuthority: string, uri: URI): Promise { await this._startCalled.wait(); if (this._actual) { return this._actual.getCanonicalURI(remoteAuthority, uri); diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index f3acf4420a0..d021dda0f0d 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -27,7 +27,10 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface IExtensionHostProxy { resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; - getCanonicalURI(remoteAuthority: string, uri: URI): Promise; + /** + * Returns `null` if no resolver for `remoteAuthority` is found. + */ + getCanonicalURI(remoteAuthority: string, uri: URI): Promise; startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; extensionTestsExecute(): Promise; extensionTestsExit(code: number): Promise; diff --git a/src/vs/workbench/services/extensions/common/extensionPoints.ts b/src/vs/workbench/services/extensions/common/extensionPoints.ts index 887640645d6..5f3f9e0d547 100644 --- a/src/vs/workbench/services/extensions/common/extensionPoints.ts +++ b/src/vs/workbench/services/extensions/common/extensionPoints.ts @@ -12,9 +12,10 @@ import * as arrays from 'vs/base/common/arrays'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { getGalleryExtensionId, groupByExtension, ExtensionIdentifierWithVersion, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { getGalleryExtensionId, getExtensionId, ExtensionKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { isValidExtensionVersion } from 'vs/platform/extensions/common/extensionValidator'; -import { ExtensionIdentifier, IExtensionDescription, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription, TargetPlatform, UNDEFINED_PUBLISHER } from 'vs/platform/extensions/common/extensions'; +import { Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; const MANIFEST_FILE = 'package.json'; @@ -117,7 +118,7 @@ abstract class ExtensionManifestHandler { class ExtensionManifestParser extends ExtensionManifestHandler { - private static _fastParseJSON(text: string, errors: json.ParseError[]): any { + private static _fastParseJSON(text: string, errors: json.ParseError[]): T { try { return JSON.parse(text); } catch (err) { @@ -126,16 +127,15 @@ class ExtensionManifestParser extends ExtensionManifestHandler { } } - public parse(): Promise { + public parse(): Promise { return this._host.readFile(this._absoluteManifestPath).then((manifestContents) => { const errors: json.ParseError[] = []; - const manifest = ExtensionManifestParser._fastParseJSON(manifestContents, errors); + const manifest = ExtensionManifestParser._fastParseJSON(manifestContents, errors); if (json.getNodeType(manifest) !== 'object') { this._error(this._absoluteFolderPath, nls.localize('jsonParseInvalidType', "Invalid manifest file {0}: Not an JSON object.", this._absoluteManifestPath)); } else if (errors.length === 0) { - if (manifest.__metadata) { - manifest.uuid = manifest.__metadata.id; - } + manifest.uuid = manifest.__metadata?.id; + manifest.targetPlatform = manifest.__metadata?.targetPlatform ?? TargetPlatform.UNDEFINED; manifest.isUserBuiltin = !!manifest.__metadata?.isBuiltin; delete manifest.__metadata; return manifest; @@ -361,24 +361,6 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { } } -// Relax the readonly properties here, it is the one place where we check and normalize values -export interface IRelaxedExtensionDescription { - id: string; - uuid?: string; - identifier: ExtensionIdentifier; - name: string; - version: string; - publisher: string; - isBuiltin: boolean; - isUserBuiltin: boolean; - isUnderDevelopment: boolean; - extensionLocation: URI; - engines: { - vscode: string; - }; - main?: string; -} - class ExtensionManifestValidator extends ExtensionManifestHandler { validate(_extensionDescription: IExtensionDescription): IExtensionDescription | null { let extensionDescription = _extensionDescription; @@ -535,6 +517,7 @@ export class ExtensionScannerInput { public readonly absoluteFolderPath: string, public readonly isBuiltin: boolean, public readonly isUnderDevelopment: boolean, + public readonly targetPlatform: TargetPlatform, public readonly translations: Translations ) { // Keep empty!! (JSON.parse) @@ -560,6 +543,7 @@ export class ExtensionScannerInput { && a.isBuiltin === b.isBuiltin && a.isUnderDevelopment === b.isUnderDevelopment && a.mtime === b.mtime + && a.targetPlatform === b.targetPlatform && Translations.equals(a.translations, b.translations) ); } @@ -658,12 +642,10 @@ export class ExtensionScanner { const nlsConfig = ExtensionScannerInput.createNLSConfig(input); let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, input.ourProductDate, host, r.path, isBuiltin, isUnderDevelopment, nlsConfig))); let extensionDescriptions = arrays.coalesce(_extensionDescriptions); - extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]); + extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionKey({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version, item.targetPlatform).toString()]); if (!isBuiltin) { - // Filter out outdated extensions - const byExtension: IExtensionDescription[][] = groupByExtension(extensionDescriptions, e => ({ id: e.identifier.value, uuid: e.uuid })); - extensionDescriptions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); + extensionDescriptions = this.filterOutdatedExtensions(extensionDescriptions, input.targetPlatform); } extensionDescriptions.sort((a, b) => { @@ -739,4 +721,22 @@ export class ExtensionScanner { return resultArr; }); } + + private static filterOutdatedExtensions(extensions: IExtensionDescription[], targetPlatform: TargetPlatform): IExtensionDescription[] { + const result = new Map(); + for (const extension of extensions) { + const extensionKey = extension.identifier.value; + const existing = result.get(extensionKey); + if (existing) { + if (semver.gt(existing.version, extension.version)) { + continue; + } + if (semver.eq(existing.version, extension.version) && existing.targetPlatform === targetPlatform) { + continue; + } + } + result.set(extensionKey, extension); + } + return [...result.values()]; + } } diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 6398b447a1a..5ab3abcb921 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -8,13 +8,14 @@ import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription, IExtensionContributions } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription, IExtensionContributions, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; -export const nullExtensionDescription = Object.freeze({ +export const nullExtensionDescription = Object.freeze({ identifier: new ExtensionIdentifier('nullExtensionDescription'), name: 'Null Extension Description', version: '0.0.0', @@ -22,6 +23,9 @@ export const nullExtensionDescription = Object.freeze({ engines: { vscode: '' }, extensionLocation: URI.parse('void:location'), isBuiltin: false, + targetPlatform: TargetPlatform.UNDEFINED, + isUserBuiltin: false, + isUnderDevelopment: false, }); export type WebWorkerExtHostConfigValue = boolean | 'auto'; @@ -36,31 +40,48 @@ export interface IMessage { extensionPointId: string; } -export const enum ExtensionRunningLocation { - None, - LocalProcess, - LocalWebWorker, - Remote -} - -export function extensionRunningLocationToString(location: ExtensionRunningLocation) { - switch (location) { - case ExtensionRunningLocation.None: - return 'None'; - case ExtensionRunningLocation.LocalProcess: +export class LocalProcessRunningLocation { + public readonly kind = ExtensionHostKind.LocalProcess; + constructor( + public readonly affinity: number + ) { } + public equals(other: ExtensionRunningLocation) { + return (this.kind === other.kind && this.affinity === other.affinity); + } + public asString(): string { + if (this.affinity === 0) { return 'LocalProcess'; - case ExtensionRunningLocation.LocalWebWorker: - return 'LocalWebWorker'; - case ExtensionRunningLocation.Remote: - return 'Remote'; + } + return `LocalProcess${this.affinity}`; } } +export class LocalWebWorkerRunningLocation { + public readonly kind = ExtensionHostKind.LocalWebWorker; + public readonly affinity = 0; + public equals(other: ExtensionRunningLocation) { + return (this.kind === other.kind); + } + public asString(): string { + return 'LocalWebWorker'; + } +} +export class RemoteRunningLocation { + public readonly kind = ExtensionHostKind.Remote; + public readonly affinity = 0; + public equals(other: ExtensionRunningLocation) { + return (this.kind === other.kind); + } + public asString(): string { + return 'Remote'; + } +} +export type ExtensionRunningLocation = LocalProcessRunningLocation | LocalWebWorkerRunningLocation | RemoteRunningLocation; export interface IExtensionsStatus { messages: IMessage[]; activationTimes: ActivationTimes | undefined; runtimeErrors: Error[]; - runningLocation: ExtensionRunningLocation; + runningLocation: ExtensionRunningLocation | null; } export class MissingExtensionDependency { @@ -108,12 +129,15 @@ export interface IExtensionHostProfile { } export const enum ExtensionHostKind { - LocalProcess, - LocalWebWorker, - Remote + LocalProcess = 1, + LocalWebWorker = 2, + Remote = 3 } -export function extensionHostKindToString(kind: ExtensionHostKind): string { +export function extensionHostKindToString(kind: ExtensionHostKind | null): string { + if (kind === null) { + return 'None'; + } switch (kind) { case ExtensionHostKind.LocalProcess: return 'LocalProcess'; case ExtensionHostKind.LocalWebWorker: return 'LocalWebWorker'; @@ -122,9 +146,14 @@ export function extensionHostKindToString(kind: ExtensionHostKind): string { } export interface IExtensionHost { - readonly kind: ExtensionHostKind; + readonly runningLocation: ExtensionRunningLocation; readonly remoteAuthority: string | null; readonly lazyStart: boolean; + /** + * A collection of extensions that will execute or are executing on this extension host. + * **NOTE**: this will reflect extensions correctly only after `start()` resolves. + */ + readonly extensions: ExtensionDescriptionRegistry; readonly onExit: Event<[number, string | null]>; start(): Promise | null; @@ -186,6 +215,8 @@ export interface IWillActivateEvent { } export interface IResponsiveStateChangeEvent { + extensionHostId: string; + extensionHostKind: ExtensionHostKind; isResponsive: boolean; } @@ -288,10 +319,15 @@ export interface IExtensionService { getExtensionsStatus(): { [id: string]: IExtensionsStatus }; /** - * Return the inspect port or `0`, the latter means inspection - * is not possible. + * Return the inspect port or `0` for a certain extension host. + * `0` means inspection is not possible. */ - getInspectPort(tryEnableInspector: boolean): Promise; + getInspectPort(extensionHostId: string, tryEnableInspector: boolean): Promise; + + /** + * Return the inspect ports (if inspection is possible) for extension hosts of kind `extensionHostKind`. + */ + getInspectPorts(extensionHostKind: ExtensionHostKind, tryEnableInspector: boolean): Promise; /** * Stops the extension hosts. @@ -340,6 +376,7 @@ export function toExtension(extensionDescription: IExtensionDescription): IExten identifier: { id: getGalleryExtensionId(extensionDescription.publisher, extensionDescription.name), uuid: extensionDescription.uuid }, manifest: extensionDescription, location: extensionDescription.extensionLocation, + targetPlatform: extensionDescription.targetPlatform, }; } @@ -351,7 +388,8 @@ export function toExtensionDescription(extension: IExtension, isUnderDevelopment isUnderDevelopment: !!isUnderDevelopment, extensionLocation: extension.location, ...extension.manifest, - uuid: extension.identifier.uuid + uuid: extension.identifier.uuid, + targetPlatform: extension.targetPlatform }; } @@ -370,7 +408,8 @@ export class NullExtensionService implements IExtensionService { getExtension() { return Promise.resolve(undefined); } readExtensionPointContributions(_extPoint: IExtensionPoint): Promise[]> { return Promise.resolve(Object.create(null)); } getExtensionsStatus(): { [id: string]: IExtensionsStatus } { return Object.create(null); } - getInspectPort(_tryEnableInspector: boolean): Promise { return Promise.resolve(0); } + getInspectPort(_extensionHostId: string, _tryEnableInspector: boolean): Promise { return Promise.resolve(0); } + getInspectPorts(_extensionHostKind: ExtensionHostKind, _tryEnableInspector: boolean): Promise { return Promise.resolve([]); } stopExtensionHosts(): void { } async restartExtensionHost(): Promise { } async startExtensionHosts(): Promise { } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index caac26cff71..0a1e93cded5 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -7,6 +7,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + badges: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.badges.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts', @@ -24,22 +25,23 @@ export const allApiProposals = Object.freeze({ fsChunks: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fsChunks.d.ts', idToken: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.idToken.d.ts', inlineCompletions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts', + inlineCompletionsAdditions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts', + inlineCompletionsNew: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts', + inputBoxSeverity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', - markdownStringBaseUri: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.markdownStringBaseUri.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', notebookConcatTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts', notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts', notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts', notebookDebugOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDebugOptions.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', - notebookDocumentSelector: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDocumentSelector.d.ts', + notebookDocumentEvents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts', notebookEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditor.d.ts', notebookEditorDecorationType: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorDecorationType.d.ts', notebookEditorEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts', notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', - outputChannelLanguage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.outputChannelLanguage.d.ts', portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', @@ -55,10 +57,10 @@ export const allApiProposals = Object.freeze({ 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', textDocumentNotebook: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textDocumentNotebook.d.ts', + textEditorDrop: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDrop.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', - treeViewDragAndDrop: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewDragAndDrop.d.ts', treeViewReveal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.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/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index e5294d5304a..504f8b5eb47 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -577,10 +577,6 @@ schemaRegistry.registerSchema(schemaId, schema); schemaRegistry.registerSchema(productSchemaId, { properties: { - extensionAllowedProposedApi: { - type: 'array', - deprecationMessage: nls.localize('product.extensionAllowedProposedApi', "Use `extensionEnabledApiProposals` instead.") - }, extensionEnabledApiProposals: { description: nls.localize('product.extensionEnabledApiProposals', "API proposals that the respective extensions can freely use."), type: 'object', diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index 14b907b4535..d79888c43e6 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILog } from 'vs/workbench/services/extensions/common/extensionPoints'; import { localize } from 'vs/nls'; @@ -21,13 +21,25 @@ export function dedupExtensions(system: IExtensionDescription[], user: IExtensio const extensionKey = ExtensionIdentifier.toKey(userExtension.identifier); const extension = result.get(extensionKey); if (extension) { - log.warn(userExtension.extensionLocation.fsPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + if (extension.isBuiltin) { + // Overwriting a builtin extension inherits the `isBuiltin` property and it doesn't show a warning + (userExtension).isBuiltin = true; + } else { + log.warn(userExtension.extensionLocation.fsPath, localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath)); + } } result.set(extensionKey, userExtension); }); development.forEach(developedExtension => { log.info('', localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath)); const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier); + const extension = result.get(extensionKey); + if (extension) { + if (extension.isBuiltin) { + // Overwriting a builtin extension inherits the `isBuiltin` property + (developedExtension).isBuiltin = true; + } + } result.set(extensionKey, developedExtension); }); let r: IExtensionDescription[] = []; diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index f74ca7ec9b7..bd382d7f168 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -25,9 +25,10 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { createMessageOfType, isMessageOfType, MessageType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostKind, ExtensionHostLogFileName, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; @@ -49,9 +50,9 @@ export interface IRemoteExtensionHostDataProvider { export class RemoteExtensionHost extends Disposable implements IExtensionHost { - public readonly kind = ExtensionHostKind.Remote; public readonly remoteAuthority: string; public readonly lazyStart = false; + public readonly extensions = new ExtensionDescriptionRegistry([]); private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onExit.event; @@ -62,6 +63,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private readonly _isExtensionDevHost: boolean; constructor( + public readonly runningLocation: RemoteRunningLocation, private readonly _initDataProvider: IRemoteExtensionHostDataProvider, private readonly _socketFactory: ISocketFactory, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @@ -223,6 +225,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) ); const workspace = this._contextService.getWorkspace(); + this.extensions.deltaExtensions(remoteInitData.extensions, []); return { commit: this._productService.commit, version: this._productService.version, @@ -252,7 +255,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { }, resolvedExtensions: resolvedExtensions, hostExtensions: hostExtensions, - extensions: remoteInitData.extensions, + extensions: this.extensions.getAllExtensionDescriptions(), telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: remoteInitData.extensionHostLogsPath, diff --git a/src/vs/workbench/services/extensions/common/rpcProtocol.ts b/src/vs/workbench/services/extensions/common/rpcProtocol.ts index 61491bf5c12..f44eae7e257 100644 --- a/src/vs/workbench/services/extensions/common/rpcProtocol.ts +++ b/src/vs/workbench/services/extensions/common/rpcProtocol.ts @@ -914,14 +914,11 @@ class MessageIO { } public static serializeReplyErr(req: number, err: any): VSBuffer { - if (err) { - return this._serializeReplyErrEror(req, err); + const errStr: string | undefined = (err ? safeStringify(errors.transformErrorForSerialization(err), null) : undefined); + if (typeof errStr !== 'string') { + return this._serializeReplyErrEmpty(req); } - return this._serializeReplyErrEmpty(req); - } - - private static _serializeReplyErrEror(req: number, _err: Error): VSBuffer { - const errBuff = VSBuffer.fromString(safeStringify(errors.transformErrorForSerialization(_err), null)); + const errBuff = VSBuffer.fromString(errStr); let len = 0; len += MessageBuffer.sizeLongString(errBuff); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 7642a5d3155..0a9efaa2bf4 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; +import { ILocalProcessExtensionHostDataProvider, LocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/localProcessExtensionHost'; import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { AbstractExtensionService, ExtensionRunningPreference, extensionRunningPreferenceToString } from 'vs/workbench/services/extensions/common/abstractExtensionService'; +import { AbstractExtensionService, ExtensionRunningPreference, extensionRunningPreferenceToString, filterByRunningLocation } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import * as nls from 'vs/nls'; import { runWhenIdle } from 'vs/base/common/async'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -16,13 +16,13 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsSc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig, ExtensionRunningLocation, WebWorkerExtHostConfigValue, extensionRunningLocationToString, extensionHostKindToString } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionService, toExtension, ExtensionHostKind, IExtensionHost, webWorkerExtHostConfig, ExtensionRunningLocation, WebWorkerExtHostConfigValue, extensionHostKindToString } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionHostManager } from 'vs/workbench/services/extensions/common/extensionHostManager'; import { ExtensionIdentifier, IExtension, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtensionKind } from 'vs/platform/environment/common/environment'; @@ -35,7 +35,7 @@ import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remo import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; -import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; +import { IWebWorkerExtensionHostDataProvider, WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHost'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILogService } from 'vs/platform/log/common/log'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -48,6 +48,8 @@ import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/w import { CancellationToken } from 'vs/base/common/cancellation'; import { StopWatch } from 'vs/base/common/stopwatch'; import { isCI } from 'vs/base/common/platform'; +import { IResolveAuthorityErrorResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; +import { URI } from 'vs/base/common/uri'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -56,6 +58,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten private readonly _remoteInitData: Map; private readonly _extensionScanner: CachedExtensionScanner; private readonly _crashTracker = new ExtensionHostCrashTracker(); + private _resolveAuthorityAttempt: number = 0; constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -152,13 +155,13 @@ export class ExtensionService extends AbstractExtensionService implements IExten ])); } - private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation) { + private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { return { getInitData: async () => { if (isInitialStart) { // Here we load even extensions that would be disabled by workspace trust const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); - const runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, []); + const runningLocation = this._determineRunningLocation(localExtensions); const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { autoStart: false, @@ -167,7 +170,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } else { // restart case const allExtensions = await this.getExtensions(); - const localProcessExtensions = filterByRunningLocation(allExtensions, this._runningLocation, desiredRunningLocation); + const localProcessExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, extensions: localProcessExtensions @@ -187,69 +190,70 @@ export class ExtensionService extends AbstractExtensionService implements IExten }; } - protected _pickRunningLocation(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionRunningLocation { - const result = ExtensionService.pickRunningLocation(extensionKinds, isInstalledLocally, isInstalledRemotely, preference, Boolean(this._environmentService.remoteAuthority), this._enableLocalWebWorker); - this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionRunningLocationToString(result)}`); + protected _pickExtensionHostKind(extensionId: ExtensionIdentifier, extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference): ExtensionHostKind | null { + const result = ExtensionService.pickExtensionHostKind(extensionKinds, isInstalledLocally, isInstalledRemotely, preference, Boolean(this._environmentService.remoteAuthority), this._enableLocalWebWorker); + this._logService.trace(`pickRunningLocation for ${extensionId.value}, extension kinds: [${extensionKinds.join(', ')}], isInstalledLocally: ${isInstalledLocally}, isInstalledRemotely: ${isInstalledRemotely}, preference: ${extensionRunningPreferenceToString(preference)} => ${extensionHostKindToString(result)}`); return result; } - public static pickRunningLocation(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference, hasRemoteExtHost: boolean, hasWebWorkerExtHost: boolean): ExtensionRunningLocation { - const result: ExtensionRunningLocation[] = []; + public static pickExtensionHostKind(extensionKinds: ExtensionKind[], isInstalledLocally: boolean, isInstalledRemotely: boolean, preference: ExtensionRunningPreference, hasRemoteExtHost: boolean, hasWebWorkerExtHost: boolean): ExtensionHostKind | null { + const result: ExtensionHostKind[] = []; for (const extensionKind of extensionKinds) { if (extensionKind === 'ui' && isInstalledLocally) { // ui extensions run locally if possible if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Local) { - return ExtensionRunningLocation.LocalProcess; + return ExtensionHostKind.LocalProcess; } else { - result.push(ExtensionRunningLocation.LocalProcess); + result.push(ExtensionHostKind.LocalProcess); } } if (extensionKind === 'workspace' && isInstalledRemotely) { // workspace extensions run remotely if possible if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Remote) { - return ExtensionRunningLocation.Remote; + return ExtensionHostKind.Remote; } else { - result.push(ExtensionRunningLocation.Remote); + result.push(ExtensionHostKind.Remote); } } if (extensionKind === 'workspace' && !hasRemoteExtHost) { // workspace extensions also run locally if there is no remote if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Local) { - return ExtensionRunningLocation.LocalProcess; + return ExtensionHostKind.LocalProcess; } else { - result.push(ExtensionRunningLocation.LocalProcess); + result.push(ExtensionHostKind.LocalProcess); } } if (extensionKind === 'web' && isInstalledLocally && hasWebWorkerExtHost) { // web worker extensions run in the local web worker if possible if (preference === ExtensionRunningPreference.None || preference === ExtensionRunningPreference.Local) { - return ExtensionRunningLocation.LocalWebWorker; + return ExtensionHostKind.LocalWebWorker; } else { - result.push(ExtensionRunningLocation.LocalWebWorker); + result.push(ExtensionHostKind.LocalWebWorker); } } } - return (result.length > 0 ? result[0] : ExtensionRunningLocation.None); + return (result.length > 0 ? result[0] : null); } - protected _createExtensionHosts(isInitialStart: boolean): IExtensionHost[] { - const result: IExtensionHost[] = []; - - const localProcessExtHost = this._instantiationService.createInstance(LocalProcessExtensionHost, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalProcess)); - result.push(localProcessExtHost); - - if (this._enableLocalWebWorker) { - const webWorkerExtHost = this._instantiationService.createInstance(WebWorkerExtensionHost, this._lazyLocalWebWorker, this._createLocalExtensionHostDataProvider(isInitialStart, ExtensionRunningLocation.LocalWebWorker)); - result.push(webWorkerExtHost); + protected _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { + switch (runningLocation.kind) { + case ExtensionHostKind.LocalProcess: { + return this._instantiationService.createInstance(LocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + } + case ExtensionHostKind.LocalWebWorker: { + if (this._enableLocalWebWorker) { + return this._instantiationService.createInstance(WebWorkerExtensionHost, runningLocation, this._lazyLocalWebWorker, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + } + return null; + } + case ExtensionHostKind.Remote: { + const remoteAgentConnection = this._remoteAgentService.getConnection(); + if (remoteAgentConnection) { + return this._instantiationService.createInstance(RemoteExtensionHost, runningLocation, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); + } + return null; + } } - - const remoteAgentConnection = this._remoteAgentService.getConnection(); - if (remoteAgentConnection) { - const remoteExtHost = this._instantiationService.createInstance(RemoteExtensionHost, this._createRemoteExtensionHostDataProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory); - result.push(remoteExtHost); - } - - return result; } protected override _onExtensionHostCrashed(extensionHost: IExtensionHostManager, code: number, signal: string | null): void { @@ -341,18 +345,88 @@ export class ExtensionService extends AbstractExtensionService implements IExten // --- impl + private async _resolveAuthority(remoteAuthority: string): Promise { + + const authorityPlusIndex = remoteAuthority.indexOf('+'); + if (authorityPlusIndex === -1) { + // This authority does not need to be resolved, simply parse the port number + const lastColon = remoteAuthority.lastIndexOf(':'); + return { + authority: { + authority: remoteAuthority, + host: remoteAuthority.substring(0, lastColon), + port: parseInt(remoteAuthority.substring(lastColon + 1), 10), + connectionToken: undefined + } + }; + } + + const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHosts.length === 0) { + // no local process extension hosts + throw new Error(`Cannot resolve authority`); + } + + this._resolveAuthorityAttempt++; + const results = await Promise.all(localProcessExtensionHosts.map(extHost => extHost.resolveAuthority(remoteAuthority, this._resolveAuthorityAttempt))); + + let bestErrorResult: IResolveAuthorityErrorResult | null = null; + for (const result of results) { + if (result.type === 'ok') { + return result.value; + } + if (!bestErrorResult) { + bestErrorResult = result; + continue; + } + const bestErrorIsUnknown = (bestErrorResult.error.code === RemoteAuthorityResolverErrorCode.Unknown); + const errorIsUnknown = (result.error.code === RemoteAuthorityResolverErrorCode.Unknown); + if (bestErrorIsUnknown && !errorIsUnknown) { + bestErrorResult = result; + } + } + + // we can only reach this if there is an error + throw new RemoteAuthorityResolverError(bestErrorResult!.error.message, bestErrorResult!.error.code, bestErrorResult!.error.detail); + } + + private async _getCanonicalURI(remoteAuthority: string, uri: URI): Promise { + + const authorityPlusIndex = remoteAuthority.indexOf('+'); + if (authorityPlusIndex === -1) { + // This authority does not use a resolver + return uri; + } + + const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); + if (localProcessExtensionHosts.length === 0) { + // no local process extension hosts + throw new Error(`Cannot resolve canonical URI`); + } + + const results = await Promise.all(localProcessExtensionHosts.map(extHost => extHost.getCanonicalURI(remoteAuthority, uri))); + + for (const result of results) { + if (result) { + return result; + } + } + + // we can only reach this if there was no resolver extension that can return the cannonical uri + throw new Error(`Cannot get canonical URI because no extension is installed to resolve ${getRemoteAuthorityPrefix(remoteAuthority)}`); + } + private async _resolveAuthorityAgain(): Promise { const remoteAuthority = this._environmentService.remoteAuthority; if (!remoteAuthority) { return; } - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; this._remoteAuthorityResolverService._clearResolvedAuthority(remoteAuthority); const sw = StopWatch.create(false); this._logService.info(`Invoking resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)})`); try { - const result = await localProcessExtensionHost.resolveAuthority(remoteAuthority); + const result = await this._resolveAuthority(remoteAuthority); this._logService.info(`resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)}) returned '${result.authority.host}:${result.authority.port}' after ${sw.elapsed()} ms`); this._remoteAuthorityResolverService._setResolvedAuthority(result.authority, result.options); } catch (err) { @@ -365,7 +439,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._extensionScanner.startScanningExtensions(this.createLogger()); const remoteAuthority = this._environmentService.remoteAuthority; - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; let remoteEnv: IRemoteAgentEnvironment | null = null; let remoteExtensions: IExtensionDescription[] = []; @@ -377,12 +450,11 @@ export class ExtensionService extends AbstractExtensionService implements IExten // The current remote authority resolver cannot give the canonical URI for this URI return uri; } - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess)!; if (isCI) { this._logService.info(`Invoking getCanonicalURI for authority ${getRemoteAuthorityPrefix(remoteAuthority)}...`); } try { - return localProcessExtensionHost.getCanonicalURI(remoteAuthority, uri); + return this._getCanonicalURI(remoteAuthority, uri); } finally { if (isCI) { this._logService.info(`getCanonicalURI returned for authority ${getRemoteAuthorityPrefix(remoteAuthority)}.`); @@ -408,14 +480,13 @@ export class ExtensionService extends AbstractExtensionService implements IExten const sw = StopWatch.create(false); this._logService.info(`Invoking resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)})`); try { - resolverResult = await localProcessExtensionHost.resolveAuthority(remoteAuthority); + resolverResult = await this._resolveAuthority(remoteAuthority); this._logService.info(`resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)}) returned '${resolverResult.authority.host}:${resolverResult.authority.port}' after ${sw.elapsed()} ms`); } catch (err) { this._logService.error(`resolveAuthority(${getRemoteAuthorityPrefix(remoteAuthority)}) returned an error after ${sw.elapsed()} ms`, err); if (RemoteAuthorityResolverError.isNoResolverFound(err)) { err.isHandled = await this._handleNoResolverFound(remoteAuthority); } else { - console.log(err); if (RemoteAuthorityResolverError.isHandled(err)) { console.log(`Error handled: Not showing a notification for the error`); } @@ -472,12 +543,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten remoteExtensions = this._checkEnabledAndProposedAPI(remoteExtensions, false); const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), false); - this._runningLocation = this._runningLocationClassifier.determineRunningLocation(localExtensions, remoteExtensions); + this._initializeRunningLocation(localExtensions, remoteExtensions); // remove non-UI extensions from the local extensions - const localProcessExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalProcess); - const localWebWorkerExtensions = filterByRunningLocation(localExtensions, this._runningLocation, ExtensionRunningLocation.LocalWebWorker); - remoteExtensions = filterByRunningLocation(remoteExtensions, this._runningLocation, ExtensionRunningLocation.Remote); + const localProcessExtensions = this._filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalProcess); + const localWebWorkerExtensions = this._filterByExtensionHostKind(localExtensions, ExtensionHostKind.LocalWebWorker); + remoteExtensions = this._filterByExtensionHostKind(remoteExtensions, ExtensionHostKind.Remote); const result = this._registry.deltaExtensions(remoteExtensions.concat(localProcessExtensions).concat(localWebWorkerExtensions), []); if (result.removedDueToLooping.length > 0) { @@ -499,23 +570,22 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._doHandleExtensionPoints(this._registry.getAllExtensionDescriptions()); - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); - if (localProcessExtensionHost) { - localProcessExtensionHost.start(localProcessExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + const localProcessExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalProcess); + const filteredLocalProcessExtensions = localProcessExtensions.filter(extension => this._registry.containsExtension(extension.identifier)); + for (const extHost of localProcessExtensionHosts) { + this._startExtensionHost(extHost, filteredLocalProcessExtensions); } - const localWebWorkerExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalWebWorker); - if (localWebWorkerExtensionHost) { - localWebWorkerExtensionHost.start(localWebWorkerExtensions.map(extension => extension.identifier).filter(id => this._registry.containsExtension(id))); + const localWebWorkerExtensionHosts = this._getExtensionHostManagers(ExtensionHostKind.LocalWebWorker); + const filteredLocalWebWorkerExtensions = localWebWorkerExtensions.filter(extension => this._registry.containsExtension(extension.identifier)); + for (const extHost of localWebWorkerExtensionHosts) { + this._startExtensionHost(extHost, filteredLocalWebWorkerExtensions); } } - public override async getInspectPort(tryEnableInspector: boolean): Promise { - const localProcessExtensionHost = this._getExtensionHostManager(ExtensionHostKind.LocalProcess); - if (localProcessExtensionHost) { - return localProcessExtensionHost.getInspectPort(tryEnableInspector); - } - return 0; + private _startExtensionHost(extensionHostManager: IExtensionHostManager, _extensions: IExtensionDescription[]): void { + const extensions = this._filterByExtensionHostManager(_extensions, extensionHostManager); + extensionHostManager.start(extensions.map(extension => extension.identifier)); } public _onExtensionHostExit(code: number): void { @@ -631,10 +701,6 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { return remoteAuthority.substring(0, plusIndex); } -function filterByRunningLocation(extensions: IExtensionDescription[], runningLocation: Map, desiredRunningLocation: ExtensionRunningLocation): IExtensionDescription[] { - return extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === desiredRunningLocation); -} - registerSingleton(IExtensionService, ExtensionService); class RestartExtensionHostAction extends Action2 { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 79556450046..2f7e9b944f6 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -34,7 +34,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { IExtensionHost, ExtensionHostLogFileName, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -44,6 +44,7 @@ import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform import { SerializedError } from 'vs/base/common/errors'; import { removeDangerousEnvVariables } from 'vs/base/node/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -105,9 +106,9 @@ class ExtensionHostProcess { export class LocalProcessExtensionHost implements IExtensionHost { - public readonly kind = ExtensionHostKind.LocalProcess; public readonly remoteAuthority = null; public readonly lazyStart = false; + public readonly extensions = new ExtensionDescriptionRegistry([]); private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onExit: Event<[number, string]> = this._onExit.event; @@ -135,6 +136,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private readonly _extensionHostLogFile: URI; constructor( + public readonly runningLocation: LocalProcessRunningLocation, private readonly _initDataProvider: ILocalProcessExtensionHostDataProvider, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @INotificationService private readonly _notificationService: INotificationService, @@ -502,6 +504,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private async _createExtHostInitData(): Promise { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); + this.extensions.deltaExtensions(initData.extensions, []); return { commit: this._productService.commit, version: this._productService.version, @@ -532,7 +535,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { }, resolvedExtensions: [], hostExtensions: [], - extensions: initData.extensions, + extensions: this.extensions.getAllExtensionDescriptions(), telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._environmentService.extHostLogsPath, diff --git a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts index 18c5b791334..9bd64f4b1a7 100644 --- a/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/cachedExtensionScanner.ts @@ -12,14 +12,15 @@ import * as platform from 'vs/base/common/platform'; import { joinPath, originalFSPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { Translations, ILog, ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver, IExtensionScannerHost, IRelaxedExtensionDescription } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { Translations, ILog, ExtensionScanner, ExtensionScannerInput, IExtensionReference, IExtensionResolver, IExtensionScannerHost } from 'vs/workbench/services/extensions/common/extensionPoints'; import { dedupExtensions } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { FileOperationResult, IFileService, toFileOperationResult } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; interface IExtensionCacheData { input: ExtensionScannerInput; @@ -54,7 +55,8 @@ export class CachedExtensionScanner { @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, - @IFileService private readonly _fileService: IFileService + @IFileService private readonly _fileService: IFileService, + @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService ) { this.scannedExtensions = new Promise((resolve, reject) => { this._scannedExtensionsResolve = resolve; @@ -108,7 +110,8 @@ export class CachedExtensionScanner { const date = this._productService.date; const devMode = !this._environmentService.isBuilt; const locale = platform.language; - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, translations); + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, path, isBuiltin, false, targetPlatform, translations); return ExtensionScanner.scanSingleExtension(input, this._createExtensionScannerHost(log)); } @@ -247,7 +250,7 @@ export class CachedExtensionScanner { return Object.create(null); } - private _scanInstalledExtensions( + private async _scanInstalledExtensions( log: ILog, translations: Translations ): Promise<{ system: IExtensionDescription[]; user: IExtensionDescription[]; development: IExtensionDescription[] }> { @@ -257,10 +260,11 @@ export class CachedExtensionScanner { const date = this._productService.date; const devMode = !this._environmentService.isBuilt; const locale = platform.language; + const targetPlatform = await this._extensionManagementService.getTargetPlatform(); const builtinExtensions = this._scanExtensionsWithCache( BUILTIN_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, getSystemExtensionsRoot(), true, false, targetPlatform, translations), log ); @@ -273,7 +277,7 @@ export class CachedExtensionScanner { const controlFile = this._fileService.readFile(URI.file(controlFilePath)) .then(raw => JSON.parse(raw.value.toString()), () => ({} as any)); - const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations); + const input = new ExtensionScannerInput(version, date, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, targetPlatform, translations); const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile]) .then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control)) .then(resolver => ExtensionScanner.scanExtensions(input, this._createExtensionScannerHost(log), resolver)); @@ -283,7 +287,7 @@ export class CachedExtensionScanner { const userExtensions = (this._scanExtensionsWithCache( USER_MANIFEST_CACHE_FILE, - new ExtensionScannerInput(version, date, commit, locale, devMode, this._environmentService.extensionsPath, false, false, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, this._environmentService.extensionsPath, false, false, targetPlatform, translations), log )); @@ -292,7 +296,7 @@ export class CachedExtensionScanner { if (this._environmentService.isExtensionDevelopment && this._environmentService.extensionDevelopmentLocationURI) { const extDescsP = this._environmentService.extensionDevelopmentLocationURI.filter(extLoc => extLoc.scheme === Schemas.file).map(extLoc => { return ExtensionScanner.scanOneOrMultipleExtensions( - new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, translations), + new ExtensionScannerInput(version, date, commit, locale, devMode, originalFSPath(extLoc), false, true, targetPlatform, translations), this._createExtensionScannerHost(log) ); }); diff --git a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts index ec46f3b43be..ecc2877518c 100644 --- a/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts +++ b/src/vs/workbench/services/extensions/test/browser/extensionService.test.ts @@ -6,84 +6,84 @@ import * as assert from 'assert'; import { ExtensionService as BrowserExtensionService } from 'vs/workbench/services/extensions/browser/extensionService'; import { ExtensionRunningPreference } from 'vs/workbench/services/extensions/common/abstractExtensionService'; -import { ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; suite('BrowserExtensionService', () => { test('pickRunningLocation', () => { - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.None); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], false, true, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], true, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation([], true, true, ExtensionRunningPreference.None), null); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'web', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['ui', 'workspace', 'web'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'ui', 'workspace'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['web', 'workspace', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, false, ExtensionRunningPreference.None), ExtensionRunningLocation.None); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionRunningLocation.LocalWebWorker); - assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionRunningLocation.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'ui', 'web'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, false, ExtensionRunningPreference.None), null); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], false, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, false, ExtensionRunningPreference.None), ExtensionHostKind.LocalWebWorker); + assert.deepStrictEqual(BrowserExtensionService.pickRunningLocation(['workspace', 'web', 'ui'], true, true, ExtensionRunningPreference.None), ExtensionHostKind.Remote); }); }); diff --git a/src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts b/src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts index f70257dafd7..da0a8f5a574 100644 --- a/src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts +++ b/src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts @@ -90,7 +90,7 @@ export class NestedWorker extends EventTarget implements Worker { type: '_terminateWorker', id }; - channel.port1.postMessage(msg); + nativePostMessage(msg); URL.revokeObjectURL(blobUrl); channel.port1.close(); diff --git a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html index fbf718a6253..11135d5fff1 100644 --- a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html +++ b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html @@ -4,7 +4,7 @@ @@ -13,6 +13,45 @@ const searchParams = new URL(document.location.href).searchParams; const vscodeWebWorkerExtHostId = searchParams.get('vscodeWebWorkerExtHostId') || ''; const name = searchParams.get('debugged') ? 'DebugWorkerExtensionHost' : 'WorkerExtensionHost'; + const parentOrigin = searchParams.get('parentOrigin') || window.origin; + const salt = searchParams.get('salt'); + + (async function() { + const hostnameValidationMarker = 'v--'; + const hostname = location.hostname; + if (!hostname.startsWith(hostnameValidationMarker)) { + // validation not requested + return start(); + } + if (!crypto.subtle) { + // cannot validate, not running in a secure context + return sendError(new Error(`Cannot validate in current context!`)); + } + + // Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined + // compute a sha-256 composed of `parentOrigin` and `salt` converted to base 32 + let parentOriginHash; + try { + const strData = JSON.stringify({ parentOrigin, salt }); + const encoder = new TextEncoder(); + const arrData = encoder.encode(strData); + const hash = await crypto.subtle.digest('sha-256', arrData); + const hashArray = Array.from(new Uint8Array(hash)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + // sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32 + parentOriginHash = BigInt(`0x${hashHex}`).toString(32).padStart(52, '0'); + } catch(err) { + return sendError(err instanceof Error ? err : new Error(String(err))); + } + + const requiredSubdomain = `${hostnameValidationMarker}${parentOriginHash}.`; + if (hostname.substring(0, requiredSubdomain.length) === requiredSubdomain) { + // validation succeeded! + return start(); + } + + return sendError(new Error(`Expected '${requiredSubdomain}' as subdomain!`)); + })(); function sendError(error) { window.parent.postMessage({ @@ -25,45 +64,52 @@ }, '*'); } - try { - const worker = new Worker('../../../../base/worker/workerMain.js', { name }); - worker.postMessage('vs/workbench/api/worker/extensionHostWorker'); - const nestedWorkers = new Map(); + function start() { + try { + const worker = new Worker('../../../../base/worker/workerMain.js', { name }); + worker.postMessage('vs/workbench/api/worker/extensionHostWorker'); + const nestedWorkers = new Map(); - worker.onmessage = (event) => { - const { data } = event; + worker.onmessage = (event) => { + const { data } = event; - if (data?.type === '_newWorker') { - const { id, port, url, options } = data; - const newWorker = new Worker(url, options); - newWorker.postMessage(port, [port]); - worker.onerror = console.error.bind(console); - nestedWorkers.set(id, newWorker); + if (data?.type === '_newWorker') { + const { id, port, url, options } = data; + const newWorker = new Worker(url, options); + newWorker.postMessage(port, [port]); + worker.onerror = console.error.bind(console); + nestedWorkers.set(id, newWorker); - } else if (data?.type === '_terminateWorker') { - const { id } = data; - if(nestedWorkers.has(id)) { - nestedWorkers.get(id).terminate(); - nestedWorkers.delete(id); + } else if (data?.type === '_terminateWorker') { + const { id } = data; + if(nestedWorkers.has(id)) { + nestedWorkers.get(id).terminate(); + nestedWorkers.delete(id); + } + } else { + worker.onerror = console.error.bind(console); + window.parent.postMessage({ + vscodeWebWorkerExtHostId, + data + }, parentOrigin, [data]); } - } else { - worker.onerror = console.error.bind(console); - window.parent.postMessage({ - vscodeWebWorkerExtHostId, - data - }, '*', [data]); + }; + + worker.onerror = (event) => { + console.error(event.message, event.error); + sendError(event.error); + }; + + self.onmessage = (event) => { + if (event.origin !== parentOrigin) { + return; + } + worker.postMessage(event.data, event.ports); } - }; - - worker.onerror = (event) => { - console.error(event.message, event.error); - sendError(event.error); - }; - - self.onmessage = (event) => worker.postMessage(event.data, event.ports); - } catch(err) { - console.error(err); - sendError(err); + } catch(err) { + console.error(err); + sendError(err); + } } })(); diff --git a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts index 9267985ac34..68171dcb5a0 100644 --- a/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files/electron-sandbox/diskFileSystemProvider.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { isLinux } from 'vs/base/common/platform'; -import { FileSystemProviderCapabilities, FileDeleteOptions, IStat, FileType, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, FileAtomicReadOptions } from 'vs/platform/files/common/files'; +import { FileSystemProviderCapabilities, FileDeleteOptions, IStat, FileType, FileReadStreamOptions, FileWriteOptions, FileOpenOptions, FileOverwriteOptions, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileAtomicReadCapability, FileAtomicReadOptions, IFileSystemProviderWithFileCloneCapability } from 'vs/platform/files/common/files'; import { AbstractDiskFileSystemProvider } from 'vs/platform/files/common/diskFileSystemProvider'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -27,7 +27,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileFolderCopyCapability, - IFileSystemProviderWithFileAtomicReadCapability { + IFileSystemProviderWithFileAtomicReadCapability, + IFileSystemProviderWithFileCloneCapability { private readonly provider = this._register(new DiskFileSystemProviderClient(this.mainProcessService.getChannel(LOCAL_FILE_SYSTEM_CHANNEL_NAME), { pathCaseSensitive: isLinux, trash: true })); @@ -120,6 +121,14 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple //#endregion + //#region Clone File + + cloneFile(from: URI, to: URI): Promise { + return this.provider.cloneFile(from, to); + } + + //#endregion + //#region File Watching protected createUniversalWatcher( diff --git a/src/vs/workbench/services/history/browser/historyService.ts b/src/vs/workbench/services/history/browser/historyService.ts index d872c2fd669..77bc578c7cc 100644 --- a/src/vs/workbench/services/history/browser/historyService.ts +++ b/src/vs/workbench/services/history/browser/historyService.ts @@ -1347,9 +1347,9 @@ export class EditorNavigationStack extends Disposable { let entryLabels: string[] = []; for (const entry of this.stack) { if (typeof entry.selection?.log === 'function') { - entryLabels.push(`- group: ${entry.groupId}, editor: ${entry.editor.resource?.toString(true)}, selection: ${entry.selection.log()}`); + entryLabels.push(`- group: ${entry.groupId}, editor: ${entry.editor.resource?.toString()}, selection: ${entry.selection.log()}`); } else { - entryLabels.push(`- group: ${entry.groupId}, editor: ${entry.editor.resource?.toString(true)}, selection: `); + entryLabels.push(`- group: ${entry.groupId}, editor: ${entry.editor.resource?.toString()}, selection: `); } } @@ -1388,7 +1388,7 @@ ${entryLabels.join('\n')} } if (editor !== null) { - this.logService.trace(`[History stack ${filterLabel}-${scopeLabel}]: ${msg} (editor: ${editor?.resource?.toString(true)}, event: ${this.traceEvent(event)})`); + this.logService.trace(`[History stack ${filterLabel}-${scopeLabel}]: ${msg} (editor: ${editor?.resource?.toString()}, event: ${this.traceEvent(event)})`); } else { this.logService.trace(`[History stack ${filterLabel}-${scopeLabel}]: ${msg}`); } @@ -1899,7 +1899,7 @@ class EditorHelper { const hasValidResourceEditorInputScheme = resource?.scheme === Schemas.file || resource?.scheme === Schemas.vscodeRemote || - resource?.scheme === Schemas.userData || + resource?.scheme === Schemas.vscodeUserData || resource?.scheme === this.pathService.defaultUriScheme; // Scheme is valid: prefer the untyped input diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index a9828d36e83..c22832f9755 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -68,7 +68,7 @@ export interface IHoverOptions { additionalClasses?: string[]; /** - * An optional link handler for markdown links, if this is not provided the IOpenerService will + * An optional link handler for markdown links, if this is not provided the IOpenerService will * be used to open the links using its default options. */ linkHandler?(url: string): void; @@ -85,6 +85,11 @@ export interface IHoverOptions { */ hideOnHover?: boolean; + /** + * Whether to hide the hover when a key is pressed. + */ + hideOnKeyDown?: boolean; + /** * Position of the hover. The default is to show above the target. This option will be ignored * if there is not enough room to layout the hover in the specified position, unless the diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 4071dfb6a13..7d5331927d9 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -50,9 +50,11 @@ export class HoverService implements IHoverService { } else { hoverDisposables.add(addDisposableListener(options.target, EventType.CLICK, () => this.hideHover())); } - const focusedElement = document.activeElement; - if (focusedElement) { - hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, () => this.hideHover())); + if (options.hideOnKeyDown) { + const focusedElement = document.activeElement; + if (focusedElement) { + hoverDisposables.add(addDisposableListener(focusedElement, EventType.KEY_DOWN, () => this.hideHover())); + } } if ('IntersectionObserver' in window) { diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index ba84b6b152b..00076bb9cc3 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -114,7 +114,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); instantiationService.stub(IModelService, disposables.add(instantiationService.createInstance(ModelService))); - fileService.registerProvider(Schemas.userData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.userData, new NullLogService()))); + fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); instantiationService.stub(IWorkingCopyFileService, disposables.add(instantiationService.createInstance(WorkingCopyFileService))); diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 65d814b0627..dafc93d58a3 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -269,7 +269,7 @@ export class LabelService extends Disposable implements ILabelService { getHostLabel(scheme: string, authority?: string): string { const formatter = this.findFormatting(URI.from({ scheme, authority })); - return formatter?.workspaceSuffix || ''; + return formatter?.workspaceSuffix || authority || ''; } getHostTooltip(scheme: string, authority?: string): string | undefined { diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index 77f731c7c0a..19d0da2cbd8 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -141,7 +141,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet this._logService.trace('Workspace Languages:', JSON.stringify([...this.workspaceLanguageIds])); this._logService.trace('Historical Workspace Opened Languages:', JSON.stringify([...this.historicalWorkspaceOpenedLanguageIds.keys()])); this._logService.trace('Historical Globally Opened Languages:', JSON.stringify([...this.historicalGlobalOpenedLanguageIds.keys()])); - this._logService.info('Computed Language Detection Biases:', JSON.stringify(biases)); + this._logService.trace('Computed Language Detection Biases:', JSON.stringify(biases)); this.dirtyBiases = false; this.langBiases = biases; return biases; diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 875747b9575..6915f6f035d 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -11,6 +11,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { CancellationToken } from 'vs/base/common/cancellation'; export class BrowserLifecycleService extends AbstractLifecycleService { @@ -167,6 +168,7 @@ export class BrowserLifecycleService extends AbstractLifecycleService { const logService = this.logService; this._onWillShutdown.fire({ reason: ShutdownReason.QUIT, + token: CancellationToken.None, // Unsupported in web join(promise, id) { logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); }, diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index c63c7e1dda8..199577ebfde 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -79,6 +80,12 @@ export interface WillShutdownEvent { */ readonly reason: ShutdownReason; + /** + * A token that will signal cancellation when the + * shutdown was forced by the user. + */ + readonly token: CancellationToken; + /** * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index ea1de21593d..a02dbc84a53 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -18,7 +18,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; export class NativeLifecycleService extends AbstractLifecycleService { private static readonly BEFORE_SHUTDOWN_WARNING_DELAY = 5000; - private static readonly WILL_SHUTDOWN_WARNING_DELAY = 5000; + private static readonly WILL_SHUTDOWN_WARNING_DELAY = 800; constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @@ -160,6 +160,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { this._onWillShutdown.fire({ reason, + token: cts.token, join(promise, id) { joiners.push(promise); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index d68f95d4f76..0151af752a2 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -550,7 +550,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic let setting = settingsModel.getPreference(settingKey); if (!setting && edit) { let defaultValue = (type === 'object' || type === 'array') ? this.configurationService.inspect(settingKey).defaultValue : getDefaultValue(type); - defaultValue = defaultValue === undefined && isOverrideProperty ? {} : undefined; + defaultValue = defaultValue === undefined && isOverrideProperty ? {} : defaultValue; if (defaultValue !== undefined) { const key = settingsModel instanceof WorkspaceConfigurationEditorModel ? ['settings', settingKey] : [settingKey]; await this.jsonEditingService.write(settingsModel.uri!, [{ path: key, value: defaultValue }], false); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 7b8c9cbc518..a60de6ae3c1 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -34,7 +34,8 @@ export enum SettingValueType { NullableInteger = 'nullable-integer', NullableNumber = 'nullable-number', Object = 'object', - BooleanObject = 'boolean-object' + BooleanObject = 'boolean-object', + LanguageTag = 'language-tag' } export interface ISettingsGroup { @@ -87,6 +88,9 @@ export interface ISetting { allKeysAreBoolean?: boolean; editPresentation?: EditPresentationTypes; defaultValueSource?: string | IExtensionInfo; + isLanguageTagSetting?: boolean; + categoryOrder?: number; + categoryLabel?: string; } export interface IExtensionSetting extends ISetting { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index d9a1217ef07..df0250c0d61 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -16,7 +16,7 @@ import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationPropertySchema, IConfigurationRegistry, IExtensionInfo, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry, IExtensionInfo, IRegisteredConfigurationPropertySchema, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -57,6 +57,18 @@ export abstract class AbstractSettingsModel extends EditorModel { }); } + private compareTwoNullableNumbers(a: number | undefined, b: number | undefined): number { + const aOrMax = a ?? Number.MAX_SAFE_INTEGER; + const bOrMax = b ?? Number.MAX_SAFE_INTEGER; + if (aOrMax < bOrMax) { + return -1; + } else if (aOrMax > bOrMax) { + return 1; + } else { + return 0; + } + } + filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] { const allGroups = this.filterGroups; @@ -80,12 +92,28 @@ export abstract class AbstractSettingsModel extends EditorModel { } filterMatches.sort((a, b) => { - // Sort by match type if the match types are not equal. - // The priority of the match type is given by the SettingMatchType enum. - // If they're equal, fall back to the "stable sort" counter score. if (a.matchType !== b.matchType) { + // Sort by match type if the match types are not the same. + // The priority of the match type is given by the SettingMatchType enum. return b.matchType - a.matchType; } else { + // The match types are the same. + if (a.setting.extensionInfo && b.setting.extensionInfo + && a.setting.extensionInfo.id === b.setting.extensionInfo.id) { + // These settings belong to the same extension. + if (a.setting.categoryLabel !== b.setting.categoryLabel + && (a.setting.categoryOrder !== undefined || b.setting.categoryOrder !== undefined) + && a.setting.categoryOrder !== b.setting.categoryOrder) { + // These two settings don't belong to the same category and have different category orders. + return this.compareTwoNullableNumbers(a.setting.categoryOrder, b.setting.categoryOrder); + } else if (a.setting.categoryLabel === b.setting.categoryLabel + && (a.setting.order !== undefined || b.setting.order !== undefined) + && a.setting.order !== b.setting.order) { + // These two settings belong to the same category, but have different orders. + return this.compareTwoNullableNumbers(a.setting.order, b.setting.order); + } + } + // In the worst case, go back to lexicographical order. return b.score - a.score; } }); @@ -602,7 +630,7 @@ export class DefaultSettings extends Disposable { result.push(settingsGroup); } const configurationSettings: ISetting[] = []; - for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties, config.extensionInfo)]) { + for (const setting of [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config)]) { if (!seenSettings[setting.key]) { configurationSettings.push(setting); seenSettings[setting.key] = true; @@ -629,8 +657,17 @@ export class DefaultSettings extends Disposable { return result; } - private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema }, extensionInfo?: IExtensionInfo): ISetting[] { + private parseSettings(config: IConfigurationNode): ISetting[] { const result: ISetting[] = []; + + const settingsObject = config.properties; + const extensionInfo = config.extensionInfo; + + // Try using the title if the category id wasn't given + // (in which case the category id is the same as the extension id) + const categoryLabel = config.extensionInfo?.id === config.id ? config.title : config.id; + const categoryOrder = config.order; + for (const key in settingsObject) { const prop = settingsObject[key]; if (this.matchesScope(prop)) { @@ -676,6 +713,11 @@ export class DefaultSettings extends Disposable { defaultValueSource = registeredConfigurationProp.defaultValueSource; } + let isLanguageTagSetting = false; + if (OVERRIDE_PROPERTY_REGEX.test(key)) { + isLanguageTagSetting = true; + } + result.push({ key, value, @@ -707,7 +749,10 @@ export class DefaultSettings extends Disposable { allKeysAreBoolean, editPresentation: prop.editPresentation, order: prop.order, - defaultValueSource + defaultValueSource, + isLanguageTagSetting, + categoryLabel, + categoryOrder }); } } diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 8c2d938a5a2..d82e69596f5 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -46,8 +46,8 @@ export class ProgressService extends Disposable implements IProgressService { async withProgress(options: IProgressOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void): Promise { const { location } = options; - if (typeof location === 'string') { + const handleStringLocation = (location: string) => { const viewContainer = this.viewDescriptorService.getViewContainerById(location); if (viewContainer) { const viewContainerLocation = this.viewDescriptorService.getViewContainerLocation(viewContainer); @@ -56,11 +56,15 @@ export class ProgressService extends Disposable implements IProgressService { } } - if (this.viewsService.getViewProgressIndicator(location)) { + if (this.viewDescriptorService.getViewDescriptorById(location) !== null) { return this.withViewProgress(location, task, { ...options, location }); } throw new Error(`Bad progress location: ${location}`); + }; + + if (typeof location === 'string') { + return handleStringLocation(location); } switch (location) { @@ -78,7 +82,7 @@ export class ProgressService extends Disposable implements IProgressService { case ProgressLocation.Explorer: return this.withPaneCompositeProgress('workbench.view.explorer', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Scm: - return this.withPaneCompositeProgress('workbench.view.scm', ViewContainerLocation.Sidebar, task, { ...options, location }); + return handleStringLocation('workbench.scm'); case ProgressLocation.Extensions: return this.withPaneCompositeProgress('workbench.view.extensions', ViewContainerLocation.Sidebar, task, { ...options, location }); case ProgressLocation.Dialog: diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 6642aceaed2..5d33f408514 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -46,6 +46,7 @@ export interface IRemoteAgentEnvironmentDTO { extensionHostLogsPath: UriComponents; globalStorageHome: UriComponents; workspaceStorageHome: UriComponents; + localHistoryHome: UriComponents; userHome: UriComponents; os: platform.OperatingSystem; arch: string; @@ -72,6 +73,7 @@ export class RemoteExtensionEnvironmentChannelClient { extensionHostLogsPath: URI.revive(data.extensionHostLogsPath), globalStorageHome: URI.revive(data.globalStorageHome), workspaceStorageHome: URI.revive(data.workspaceStorageHome), + localHistoryHome: URI.revive(data.localHistoryHome), userHome: URI.revive(data.userHome), os: data.os, arch: data.arch, diff --git a/src/vs/workbench/services/search/common/fileSearchManager.ts b/src/vs/workbench/services/search/common/fileSearchManager.ts index 0b39f4dd684..27453fd8138 100644 --- a/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/src/vs/workbench/services/search/common/fileSearchManager.ts @@ -10,7 +10,7 @@ import * as glob from 'vs/base/common/glob'; import * as resources from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; -import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn } from 'vs/workbench/services/search/common/search'; import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IInternalFileMatch { @@ -217,7 +217,7 @@ class FileSearchEngine { const self = this; const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { - const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); + const hasSibling = hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 6c20ba1397b..93fa38a8456 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -705,3 +705,41 @@ function hasSiblingClauses(pattern: glob.IExpression): boolean { return false; } + +export function hasSiblingPromiseFn(siblingsFn?: () => Promise) { + if (!siblingsFn) { + return undefined; + } + + let siblings: Promise>; + return (name: string) => { + if (!siblings) { + siblings = (siblingsFn() || Promise.resolve([])) + .then(list => list ? listToMap(list) : {}); + } + return siblings.then(map => !!map[name]); + }; +} + +export function hasSiblingFn(siblingsFn?: () => string[]) { + if (!siblingsFn) { + return undefined; + } + + let siblings: Record; + return (name: string) => { + if (!siblings) { + const list = siblingsFn(); + siblings = list ? listToMap(list) : {}; + } + return !!siblings[name]; + }; +} + +function listToMap(list: string[]) { + const map: Record = {}; + for (const key of list) { + map[key] = true; + } + return map; +} diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 5dd7f698495..2b6eb1befc7 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -7,13 +7,12 @@ import { flatten, mapArrayOrNot } from 'vs/base/common/arrays'; import { isThenable } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import * as glob from 'vs/base/common/glob'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; import { isArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; +import { hasSiblingPromiseFn, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; import { Range, TextSearchComplete, TextSearchMatch, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IFileUtils { @@ -125,7 +124,7 @@ export class TextSearchManager { } const hasSibling = folderQuery.folder.scheme === Schemas.file ? - glob.hasSiblingPromiseFn(() => { + hasSiblingPromiseFn(() => { return this.fileUtils.readdir(resources.dirname(result.uri)); }) : undefined; diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 265de9a10f4..60719151216 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -19,7 +19,7 @@ import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { Promises } from 'vs/base/node/pfs'; -import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; +import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch, hasSiblingFn } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { prepareQuery } from 'vs/base/common/fuzzyScorer'; @@ -422,7 +422,7 @@ export class FileWalker { const filePattern = this.filePattern; function matchDirectory(entries: IDirectoryEntry[]) { self.directoriesWalked++; - const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename)); + const hasSibling = hasSiblingFn(() => entries.map(entry => entry.basename)); for (let i = 0, n = entries.length; i < n; i++) { const entry = entries[i]; const { relativePath, basename } = entry; @@ -469,7 +469,7 @@ export class FileWalker { const rootFolder = folderQuery.folder; // Execute tasks on each file in parallel to optimize throughput - const hasSibling = glob.hasSiblingFn(() => files); + const hasSibling = hasSiblingFn(() => files); this.parallel(files, (file: string, clb: (error: Error | null, _?: any) => void): void => { // Check canceled diff --git a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts index 7fdbc01fc50..56430c94287 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts @@ -19,7 +19,7 @@ export class RipgrepSearchProvider implements TextSearchProvider { provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { const engine = new RipgrepTextSearchEngine(this.outputChannel); - if (options.folder.scheme === Schemas.userData) { + if (options.folder.scheme === Schemas.vscodeUserData) { // Ripgrep search engine can only provide file-scheme results, but we want to use it to search some schemes that are backed by the filesystem, but with some other provider as the frontend, // case in point vscode-userdata. In these cases we translate the query to a file, and translate the results back to the frontend scheme. const translatedOptions = { ...options, folder: options.folder.with({ scheme: Schemas.file }) }; diff --git a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts index 3ae847e4211..c05122f092b 100644 --- a/src/vs/workbench/services/search/test/common/ignoreFile.test.ts +++ b/src/vs/workbench/services/search/test/common/ignoreFile.test.ts @@ -477,7 +477,7 @@ suite('Parsing .gitignore files', () => { }); runTest({ - pattern: `[._]*.s[a-w][a-z] + pattern: `[._]*s[a-w][a-z] [._]s[a-w][a-z] *.un~ *~`, diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 531782d63ef..c65ee021e76 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -124,11 +124,11 @@ export class TelemetryService extends Disposable implements ITelemetryService { const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new WebAppInsightsAppender('monacoworkbench', productService.aiConfig?.asimovKey); const config: ITelemetryServiceConfig = { appenders: [new WebTelemetryAppender(telemetryProvider), new TelemetryLogAppender(loggerService, environmentService)], - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.remoteAuthority, productService.embedderIdentifier, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.remoteAuthority, productService.embedderIdentifier, productService.removeTelemetryMachineId, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), sendErrorTelemetry: this.sendErrorTelemetry, }; - this.impl = this._register(new BaseTelemetryService(config, configurationService)); + this.impl = this._register(new BaseTelemetryService(config, configurationService, productService)); } else { this.impl = NullTelemetryService; } diff --git a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts index cd56911dba6..69116d9a458 100644 --- a/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/browser/workbenchCommonProperties.ts @@ -26,18 +26,25 @@ export async function resolveWorkbenchCommonProperties( version: string | undefined, remoteAuthority?: string, productIdentifier?: string, + removeMachineId?: boolean, resolveAdditionalProperties?: () => { [key: string]: any } ): Promise<{ [name: string]: string | undefined }> { const result: { [name: string]: string | undefined } = Object.create(null); const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; - let machineId = storageService.get(machineIdKey, StorageScope.GLOBAL); - if (!machineId) { - machineId = uuid.generateUuid(); - storageService.store(machineIdKey, machineId, StorageScope.GLOBAL, StorageTarget.MACHINE); + let machineId: string | undefined; + if (!removeMachineId) { + machineId = storageService.get(machineIdKey, StorageScope.GLOBAL); + if (!machineId) { + machineId = uuid.generateUuid(); + storageService.store(machineIdKey, machineId, StorageScope.GLOBAL, StorageTarget.MACHINE); + } + } else { + machineId = `Redacted-${productIdentifier ?? 'web'}`; } + /** * Note: In the web, session date information is fetched from browser storage, so these dates are tied to a specific * browser and not the machine overall. diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index bd20ddf7216..ca23c70a78f 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -44,7 +44,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { sendErrorTelemetry: true }; - this.impl = this._register(new BaseTelemetryService(config, configurationService)); + this.impl = this._register(new BaseTelemetryService(config, configurationService, productService)); } else { this.impl = NullTelemetryService; } diff --git a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts index d2a217d2d80..c3f2b77b696 100644 --- a/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts @@ -23,7 +23,7 @@ suite('Browser Telemetry - common properties', function () { }; }; - const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, resolveCommonTelemetryProperties); + const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); assert.ok('commitHash' in props); assert.ok('sessionID' in props); @@ -53,10 +53,10 @@ suite('Browser Telemetry - common properties', function () { }); }; - const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, resolveCommonTelemetryProperties); + const props = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); assert.strictEqual(props['userId'], 1); - const props2 = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, resolveCommonTelemetryProperties); + const props2 = await resolveWorkbenchCommonProperties(testStorageService, commit, version, undefined, undefined, false, resolveCommonTelemetryProperties); assert.strictEqual(props2['userId'], 2); }); }); diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index fd04e1fd157..57aa1cd2c21 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -26,7 +26,7 @@ import type { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmat import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IValidGrammarDefinition, IValidEmbeddedLanguagesMap, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; -import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; +import { missingTMGrammarErrorMessage, TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { TMTokenization } from 'vs/workbench/services/textMate/common/TMTokenization'; @@ -266,6 +266,10 @@ export abstract class AbstractTextMateService extends Disposable implements ITex }); return new TMTokenizationSupportWithLineLimit(languageId, encodedLanguageId, tokenization, this._configurationService); } catch (err) { + if (err.message && err.message === missingTMGrammarErrorMessage) { + // Don't log this error message + return null; + } onUnexpectedError(err); return null; } diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index 5d5ae7a8c8e..2bb9be17ad0 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import type { IGrammar, Registry, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -22,6 +21,8 @@ export interface ICreateGrammarResult { containsEmbeddedLanguages: boolean; } +export const missingTMGrammarErrorMessage = 'No TM Grammar registered for this language.'; + export class TMGrammarFactory extends Disposable { private readonly _host: ITMGrammarFactoryHost; @@ -113,13 +114,13 @@ export class TMGrammarFactory extends Disposable { const scopeName = this._languageToScope.get(languageId); if (typeof scopeName !== 'string') { // No TM grammar defined - return Promise.reject(new Error(nls.localize('no-tm-grammar', "No TM Grammar registered for this language."))); + throw new Error(missingTMGrammarErrorMessage); } const grammarDefinition = this._scopeRegistry.getGrammarDefinition(scopeName); if (!grammarDefinition) { // No TM grammar defined - return Promise.reject(new Error(nls.localize('no-tm-grammar', "No TM Grammar registered for this language."))); + throw new Error(missingTMGrammarErrorMessage); } let embeddedLanguages = grammarDefinition.embeddedLanguages; @@ -134,7 +135,17 @@ export class TMGrammarFactory extends Disposable { const containsEmbeddedLanguages = (Object.keys(embeddedLanguages).length > 0); - const grammar = await this._grammarRegistry.loadGrammarWithConfiguration(scopeName, encodedLanguageId, { embeddedLanguages, tokenTypes: grammarDefinition.tokenTypes }); + let grammar: IGrammar | null; + + try { + grammar = await this._grammarRegistry.loadGrammarWithConfiguration(scopeName, encodedLanguageId, { embeddedLanguages, tokenTypes: grammarDefinition.tokenTypes }); + } catch (err) { + if (err.message && err.message.startsWith('No grammar provided for')) { + // No TM grammar defined + throw new Error(missingTMGrammarErrorMessage); + } + throw err; + } return { languageId: languageId, diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 29945de6885..7a92fa6e6fa 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { IEncodingSupport, ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding, stringToSnapshot, ITextFileSaveAsOptions, IReadTextFileEncodingOptions, TextFileEditorModelState } from 'vs/workbench/services/textfile/common/textfiles'; -import { IRevertOptions } from 'vs/workbench/common/editor'; +import { IRevertOptions, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, IFileStreamContent } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -50,6 +50,9 @@ export abstract class AbstractTextFileService extends Disposable implements ITex declare readonly _serviceBrand: undefined; + private static readonly TEXTFILE_SAVE_CREATE_SOURCE = SaveSourceRegistry.registerSource('textFileCreate.source', localize('textFileCreate.source', "File Created")); + private static readonly TEXTFILE_SAVE_REPLACE_SOURCE = SaveSourceRegistry.registerSource('textFileOverwrite.source', localize('textFileOverwrite.source', "File Replaced")); + readonly files: ITextFileEditorModelManager = this._register(this.instantiationService.createInstance(TextFileEditorModelManager)); readonly untitled: IUntitledTextEditorModelManager = this.untitledTextEditorService; @@ -536,6 +539,14 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } } + // set source options depending on target exists or not + if (!options?.source) { + options = { + ...options, + source: targetExists ? AbstractTextFileService.TEXTFILE_SAVE_REPLACE_SOURCE : AbstractTextFileService.TEXTFILE_SAVE_CREATE_SOURCE + }; + } + // save model return targetModel.save(options); } @@ -576,7 +587,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // of untitled model if it is a valid path name, // otherwise fallback to `basename`. let untitledName = model.name; - if (!(await this.pathService.hasValidBasename(joinPath(defaultFilePath, untitledName)))) { + if (!(await this.pathService.hasValidBasename(joinPath(defaultFilePath, untitledName), untitledName))) { untitledName = basename(resource); } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 9282addc742..da6f65d79f8 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -6,7 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; -import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; @@ -58,7 +58,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private readonly _onDidSaveError = this._register(new Emitter()); readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); readonly onDidSave = this._onDidSave.event; private readonly _onDidRevert = this._register(new Emitter()); @@ -269,11 +269,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Resolve override async resolve(options?: ITextFileResolveOptions): Promise { - this.logService.trace('[text file model] resolve() - enter', this.resource.toString(true)); + this.trace('[text file model] resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.logService.trace('[text file model] resolve() - exit - without resolving because model is disposed', this.resource.toString(true)); + this.trace('[text file model] resolve() - exit - without resolving because model is disposed'); return; } @@ -282,7 +282,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // resolve a model that is dirty or is in the process of saving to prevent data // loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.logService.trace('[text file model] resolve() - exit - without resolving because model is dirty or being saved', this.resource.toString(true)); + this.trace('[text file model] resolve() - exit - without resolving because model is dirty or being saved'); return; } @@ -311,7 +311,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromBuffer(buffer: ITextBufferFactory, options?: ITextFileResolveOptions): Promise { - this.logService.trace('[text file model] resolveFromBuffer()', this.resource.toString(true)); + this.trace('[text file model] resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -369,7 +369,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Abort if someone else managed to resolve the model by now let isNewModel = !this.isResolved(); if (!isNewModel) { - this.logService.trace('[text file model] resolveFromBackup() - exit - without resolving because previously new model got created meanwhile', this.resource.toString(true)); + this.trace('[text file model] resolveFromBackup() - exit - without resolving because previously new model got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -386,7 +386,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup, encoding: string, options?: ITextFileResolveOptions): Promise { - this.logService.trace('[text file model] doResolveFromBackup()', this.resource.toString(true)); + this.trace('[text file model] doResolveFromBackup()'); // Resolve with backup this.resolveFromContent({ @@ -408,7 +408,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private async resolveFromFile(options?: ITextFileResolveOptions): Promise { - this.logService.trace('[text file model] resolveFromFile()', this.resource.toString(true)); + this.trace('[text file model] resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; const allowBinary = this.isResolved() /* always allow if we resolved previously */ || options?.allowBinary; @@ -435,7 +435,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Return early if the model content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.logService.trace('[text file model] resolveFromFile() - exit - without resolving because model content changed', this.resource.toString(true)); + this.trace('[text file model] resolveFromFile() - exit - without resolving because model content changed'); return; } @@ -472,11 +472,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private resolveFromContent(content: ITextFileStreamContent, dirty: boolean, options?: ITextFileResolveOptions): void { - this.logService.trace('[text file model] resolveFromContent() - enter', this.resource.toString(true)); + this.trace('[text file model] resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.logService.trace('[text file model] resolveFromContent() - exit - because model is disposed', this.resource.toString(true)); + this.trace('[text file model] resolveFromContent() - exit - because model is disposed'); return; } @@ -529,7 +529,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory): void { - this.logService.trace('[text file model] doCreateTextModel()', this.resource.toString(true)); + this.trace('[text file model] doCreateTextModel()'); // Create model const textModel = this.createTextEditorModel(value, resource, this.preferredLanguageId); @@ -542,7 +542,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.logService.trace('[text file model] doUpdateTextModel()', this.resource.toString(true)); + this.trace('[text file model] doUpdateTextModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -564,11 +564,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private onModelContentChanged(model: ITextModel, isUndoingOrRedoing: boolean): void { - this.logService.trace(`[text file model] onModelContentChanged() - enter`, this.resource.toString(true)); + this.trace(`[text file model] onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.logService.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString(true)); + this.trace(`[text file model] onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -585,7 +585,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.getAlternativeVersionId() === this.bufferSavedVersionId) { - this.logService.trace('[text file model] onModelContentChanged() - model content changed back to last saved version', this.resource.toString(true)); + this.trace('[text file model] onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -599,7 +599,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Otherwise the content has changed and we signal this as becoming dirty else { - this.logService.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty', this.resource.toString(true)); + this.trace('[text file model] onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -693,7 +693,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } if (this.isReadonly()) { - this.logService.trace('[text file model] save() - ignoring request for readonly resource', this.resource.toString(true)); + this.trace('[text file model] save() - ignoring request for readonly resource'); return false; // if model is readonly we do not attempt to save at all } @@ -702,15 +702,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil (this.hasState(TextFileEditorModelState.CONFLICT) || this.hasState(TextFileEditorModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.logService.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error', this.resource.toString(true)); + this.trace('[text file model] save() - ignoring auto save request for model that is in conflict or error'); return false; // if model is in save conflict or error, do not save unless save reason is explicit } // Actually do save and log - this.logService.trace('[text file model] save() - enter', this.resource.toString(true)); + this.trace('[text file model] save() - enter'); await this.doSave(options); - this.logService.trace('[text file model] save() - exit', this.resource.toString(true)); + this.trace('[text file model] save() - exit'); return this.hasState(TextFileEditorModelState.SAVED); } @@ -721,7 +721,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } let versionId = this.versionId; - this.logService.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`, this.resource.toString(true)); + this.trace(`[text file model] doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -729,7 +729,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.logService.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString(true)); + this.trace(`[text file model] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -738,7 +738,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // // Scenario: user invoked save action even though the model is not dirty if (!options.force && !this.dirty) { - this.logService.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString(true)); + this.trace(`[text file model] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -752,7 +752,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.logService.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`, this.resource.toString(true)); + this.trace(`[text file model] doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -808,7 +808,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil await this.textFileService.files.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); } } catch (error) { - this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true)); + this.logService.error(`[text file model] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString()); } } @@ -848,7 +848,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.logService.trace(`[text file model] doSave(${versionId}) - before write()`, this.resource.toString(true)); + this.trace(`[text file model] doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedTextFileEditorModel = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -876,21 +876,21 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Update dirty state unless model has changed meanwhile if (versionId === this.versionId) { - this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`, this.resource.toString(true)); + this.trace(`[text file model] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.logService.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource.toString(true)); + this.trace(`[text file model] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful this.setOrphaned(false); // Emit Save Event - this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); + this._onDidSave.fire({ reason: options.reason, stat, source: options.source }); } private handleSaveError(error: Error, versionId: number, options: ITextFileSaveOptions): void { - (options.ignoreErrorHandler ? this.logService.trace : this.logService.error)(`[text file model] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true)); + (options.ignoreErrorHandler ? this.logService.trace : this.logService.error).apply(this.logService, [`[text file model] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString()]); // Return early if the save() call was made asking to // handle the save error itself. @@ -1049,6 +1049,10 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString()); + } + override isResolved(): this is IResolvedTextFileEditorModel { return !!this.textEditorModel; } @@ -1058,7 +1062,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } override dispose(): void { - this.logService.trace('[text file model] dispose()', this.resource.toString(true)); + this.trace('[text file model] dispose()'); this.inConflictMode = false; this.inOrphanMode = false; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index 0531f1a2c7f..900a7bba75b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -485,7 +485,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeReadonly.fire(model))); modelListeners.add(model.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(model))); modelListeners.add(model.onDidSaveError(() => this._onDidSaveError.fire(model))); - modelListeners.add(model.onDidSave(reason => this._onDidSave.fire({ model, reason }))); + modelListeners.add(model.onDidSave(e => this._onDidSave.fire({ model, ...e }))); modelListeners.add(model.onDidRevert(() => this._onDidRevert.fire(model))); modelListeners.add(model.onDidChangeEncoding(() => this._onDidChangeEncoding.fire(model))); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index dedf9b0eed0..987c31e09c2 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -14,7 +14,7 @@ import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { areFunctions, isUndefinedOrNull } from 'vs/base/common/types'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; @@ -308,13 +308,24 @@ export interface ITextFileEditorModelResolveOrCreateOptions { readonly allowBinary?: boolean; } -export interface ITextFileSaveEvent { +export interface ITextFileSaveEvent extends ITextFileEditorModelSaveEvent { + + /** + * The model that was saved. + */ readonly model: ITextFileEditorModel; - readonly reason: SaveReason; } export interface ITextFileResolveEvent { + + /** + * The model that was resolved. + */ readonly model: ITextFileEditorModel; + + /** + * The reason why the model was resolved. + */ readonly reason: TextFileResolveReason; } @@ -476,9 +487,17 @@ export interface ILanguageSupport { setLanguageId(languageId: string, setExplicitly?: boolean): void; } +export interface ITextFileEditorModelSaveEvent extends IWorkingCopySaveEvent { + + /** + * The resolved stat from the save operation. + */ + readonly stat: IFileStatWithMetadata; +} + export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, ILanguageSupport, IWorkingCopy { - readonly onDidChangeContent: Event; + readonly onDidSave: Event; readonly onDidSaveError: Event; readonly onDidChangeOrphaned: Event; readonly onDidChangeReadonly: Event; diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 8581122f2d6..21bcf191cc5 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel, ITextFileEditorModelSaveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { createFileEditorInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -19,6 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; suite('Files - TextFileEditorModel', () => { @@ -94,8 +95,8 @@ suite('Files - TextFileEditorModel', () => { assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - let savedEvent = false; - model.onDidSave(() => savedEvent = true); + let savedEvent: ITextFileEditorModelSaveEvent | undefined = undefined; + model.onDidSave(e => savedEvent = e); await model.save(); assert.ok(!savedEvent); @@ -114,7 +115,8 @@ suite('Files - TextFileEditorModel', () => { } }); - const pendingSave = model.save(); + const source = SaveSourceRegistry.registerSource('testSource', 'Hello Save'); + const pendingSave = model.save({ reason: SaveReason.AUTO, source }); assert.ok(model.hasState(TextFileEditorModelState.PENDING_SAVE)); await Promise.all([pendingSave, model.joinState(TextFileEditorModelState.PENDING_SAVE)]); @@ -122,12 +124,15 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.SAVED)); assert.ok(!model.isDirty()); assert.ok(savedEvent); + assert.ok((savedEvent as ITextFileEditorModelSaveEvent).stat); + assert.strictEqual((savedEvent as ITextFileEditorModelSaveEvent).reason, SaveReason.AUTO); + assert.strictEqual((savedEvent as ITextFileEditorModelSaveEvent).source, source); assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); - savedEvent = false; + savedEvent = undefined; await model.save({ force: true }); assert.ok(savedEvent); diff --git a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts index 58ecba3461d..feca56101de 100644 --- a/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts +++ b/src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts @@ -322,9 +322,6 @@ suite('Encoding', () => { }); test('toDecodeStream - decodes buffer entirely', async function () { - if (!process.versions.electron) { - this.skip(); // TODO@bpasero enable once we ship Electron 16 - } const emojis = Buffer.from('🖥️💻💾'); const incompleteEmojis = emojis.slice(0, emojis.length - 1); diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 02030290016..702c14464c3 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -362,6 +362,7 @@ export class FileIconThemeLoader { collectSelectors(iconThemeDocument); collectSelectors(iconThemeDocument.light, '.vs'); collectSelectors(iconThemeDocument.highContrast, '.hc-black'); + collectSelectors(iconThemeDocument.highContrast, '.hc-light'); if (!result.hasFileIcons && !result.hasFolderIcons) { return result; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index b32d4a3ebed..6f8986a8963 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, ThemeSettings, IWorkbenchProductIconTheme, ThemeSettingTarget } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, VS_HC_LIGHT_THEME, ThemeSettings, IWorkbenchProductIconTheme, ThemeSettingTarget } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -66,6 +66,7 @@ function validateThemeId(theme: string): string { case VS_LIGHT_THEME: return `vs ${defaultThemeExtensionId}-themes-light_vs-json`; case VS_DARK_THEME: return `vs-dark ${defaultThemeExtensionId}-themes-dark_vs-json`; case VS_HC_THEME: return `hc-black ${defaultThemeExtensionId}-themes-hc_black-json`; + case VS_HC_LIGHT_THEME: return `hc-light ${defaultThemeExtensionId}-themes-hc_light-json`; } return theme; } @@ -242,20 +243,31 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private installConfigurationListener() { this.configurationService.onDidChangeConfiguration(e => { + let lazyPreferredColorScheme: ColorScheme | undefined | null = null; + const getPreferredColorScheme = () => { + if (lazyPreferredColorScheme === null) { + lazyPreferredColorScheme = this.getPreferredColorScheme(); + } + return lazyPreferredColorScheme; + }; + if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)) { this.restoreColorTheme(); } if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME) || e.affectsConfiguration(ThemeSettings.DETECT_HC)) { this.handlePreferredSchemeUpdated(); } - if (e.affectsConfiguration(ThemeSettings.PREFERRED_DARK_THEME) && this.getPreferredColorScheme() === ColorScheme.DARK) { + if (e.affectsConfiguration(ThemeSettings.PREFERRED_DARK_THEME) && getPreferredColorScheme() === ColorScheme.DARK) { this.applyPreferredColorTheme(ColorScheme.DARK); } - if (e.affectsConfiguration(ThemeSettings.PREFERRED_LIGHT_THEME) && this.getPreferredColorScheme() === ColorScheme.LIGHT) { + if (e.affectsConfiguration(ThemeSettings.PREFERRED_LIGHT_THEME) && getPreferredColorScheme() === ColorScheme.LIGHT) { this.applyPreferredColorTheme(ColorScheme.LIGHT); } - if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_THEME) && this.getPreferredColorScheme() === ColorScheme.HIGH_CONTRAST) { - this.applyPreferredColorTheme(ColorScheme.HIGH_CONTRAST); + if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_DARK_THEME) && getPreferredColorScheme() === ColorScheme.HIGH_CONTRAST_DARK) { + this.applyPreferredColorTheme(ColorScheme.HIGH_CONTRAST_DARK); + } + if (e.affectsConfiguration(ThemeSettings.PREFERRED_HC_LIGHT_THEME) && getPreferredColorScheme() === ColorScheme.HIGH_CONTRAST_LIGHT) { + this.applyPreferredColorTheme(ColorScheme.HIGH_CONTRAST_LIGHT); } if (e.affectsConfiguration(ThemeSettings.FILE_ICON_THEME)) { this.restoreFileIconTheme(); @@ -382,7 +394,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private getPreferredColorScheme(): ColorScheme | undefined { if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) { - return ColorScheme.HIGH_CONTRAST; + return this.hostColorService.dark ? ColorScheme.HIGH_CONTRAST_DARK : ColorScheme.HIGH_CONTRAST_LIGHT; } if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { return this.hostColorService.dark ? ColorScheme.DARK : ColorScheme.LIGHT; @@ -391,7 +403,14 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } private async applyPreferredColorTheme(type: ColorScheme): Promise { - const settingId = type === ColorScheme.DARK ? ThemeSettings.PREFERRED_DARK_THEME : type === ColorScheme.LIGHT ? ThemeSettings.PREFERRED_LIGHT_THEME : ThemeSettings.PREFERRED_HC_THEME; + let settingId: ThemeSettings; + switch (type) { + case ColorScheme.LIGHT: settingId = ThemeSettings.PREFERRED_LIGHT_THEME; break; + case ColorScheme.HIGH_CONTRAST_DARK: settingId = ThemeSettings.PREFERRED_HC_DARK_THEME; break; + case ColorScheme.HIGH_CONTRAST_LIGHT: settingId = ThemeSettings.PREFERRED_HC_LIGHT_THEME; break; + default: + settingId = ThemeSettings.PREFERRED_DARK_THEME; + } const themeSettingId = this.configurationService.getValue(settingId); if (themeSettingId && typeof themeSettingId === 'string') { const theme = this.colorThemeRegistry.findThemeBySettingsId(themeSettingId, undefined); @@ -525,7 +544,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (this.currentColorTheme.id) { this.container.classList.remove(...this.currentColorTheme.classNames); } else { - this.container.classList.remove(VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME); + this.container.classList.remove(VS_DARK_THEME, VS_LIGHT_THEME, VS_HC_THEME, VS_HC_LIGHT_THEME); } this.container.classList.add(...newTheme.classNames); diff --git a/src/vs/workbench/services/themes/common/colorExtensionPoint.ts b/src/vs/workbench/services/themes/common/colorExtensionPoint.ts index 430e9cd28b1..a693d63a16c 100644 --- a/src/vs/workbench/services/themes/common/colorExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/colorExtensionPoint.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; interface IColorExtensionPoint { id: string; description: string; - defaults: { light: string; dark: string; highContrast: string }; + defaults: { light: string; dark: string; highContrast: string; highContrastLight?: string }; } const colorRegistry: IColorRegistry = Registry.as(ColorRegistryExtensions.ColorContribution); @@ -114,10 +114,18 @@ export class ColorExtensionPoint { collector.error(nls.localize('invalid.defaults', "'configuration.colors.defaults' must be defined and must contain 'light', 'dark' and 'highContrast'")); return; } + if (defaults.highContrastLight === undefined) { + collector.warn(nls.localize('missing.defaults.highContrastLight', "color contribution {0} does not provide color 'defaults.highContrastLight'. Using color for `light` instead ({1}).", colorContribution.id, defaults.light)); + } else if (typeof defaults.highContrastLight !== 'string') { + collector.error(nls.localize('invalid.defaults.highContrastLight', "'configuration.colors.defaults.highContrastLight' must a string.")); + return; + } + colorRegistry.registerColor(colorContribution.id, { light: parseColorValue(defaults.light, 'configuration.colors.defaults.light'), dark: parseColorValue(defaults.dark, 'configuration.colors.defaults.dark'), - hc: parseColorValue(defaults.highContrast, 'configuration.colors.defaults.highContrast') + hcDark: parseColorValue(defaults.highContrast, 'configuration.colors.defaults.highContrast'), + hcLight: parseColorValue(defaults.highContrastLight ?? defaults.light, 'configuration.colors.defaults.highContrastLight'), }, colorContribution.description); } } diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index b2b88468d5e..9af7e58c941 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -6,7 +6,7 @@ import { basename } from 'vs/base/common/path'; import * as Json from 'vs/base/common/json'; import { Color } from 'vs/base/common/color'; -import { ExtensionData, ITokenColorCustomizations, ITextMateThemingRule, IWorkbenchColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations, ISemanticTokenRules, ISemanticTokenColorizationSetting, ISemanticTokenColorCustomizations, IThemeScopableCustomizations, IThemeScopedCustomizations, THEME_SCOPE_CLOSE_PAREN, THEME_SCOPE_OPEN_PAREN, themeScopeRegex, THEME_SCOPE_WILDCARD } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ExtensionData, ITokenColorCustomizations, ITextMateThemingRule, IWorkbenchColorTheme, IColorMap, IThemeExtensionPoint, VS_LIGHT_THEME, VS_HC_THEME, IColorCustomizations, ISemanticTokenRules, ISemanticTokenColorizationSetting, ISemanticTokenColorCustomizations, IThemeScopableCustomizations, IThemeScopedCustomizations, THEME_SCOPE_CLOSE_PAREN, THEME_SCOPE_OPEN_PAREN, themeScopeRegex, THEME_SCOPE_WILDCARD, VS_HC_LIGHT_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { convertSettings } from 'vs/workbench/services/themes/common/themeCompatibility'; import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; @@ -578,7 +578,8 @@ export class ColorThemeData implements IWorkbenchColorTheme { get type(): ColorScheme { switch (this.baseTheme) { case VS_LIGHT_THEME: return ColorScheme.LIGHT; - case VS_HC_THEME: return ColorScheme.HIGH_CONTRAST; + case VS_HC_THEME: return ColorScheme.HIGH_CONTRAST_DARK; + case VS_HC_LIGHT_THEME: return ColorScheme.HIGH_CONTRAST_LIGHT; default: return ColorScheme.DARK; } } @@ -780,12 +781,18 @@ let defaultThemeColors: { [baseTheme: string]: ITextMateThemingRule[] } = { { scope: 'token.error-token', settings: { foreground: '#f44747' } }, { scope: 'token.debug-token', settings: { foreground: '#b267e6' } } ], - 'hc': [ + 'hcLight': [ + { scope: 'token.info-token', settings: { foreground: '#316bcd' } }, + { scope: 'token.warn-token', settings: { foreground: '#cd9731' } }, + { scope: 'token.error-token', settings: { foreground: '#cd3131' } }, + { scope: 'token.debug-token', settings: { foreground: '#800080' } } + ], + 'hcDark': [ { scope: 'token.info-token', settings: { foreground: '#6796e6' } }, { scope: 'token.warn-token', settings: { foreground: '#008000' } }, { scope: 'token.error-token', settings: { foreground: '#FF0000' } }, { scope: 'token.debug-token', settings: { foreground: '#b267e6' } } - ], + ] }; const noMatch = (_scope: ProbeScope) => -1; diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 364d1bee23a..8f33a4418f3 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -18,7 +18,8 @@ import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; -const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast'; +const DEFAULT_THEME_HC_DARK_SETTING_VALUE = 'Default High Contrast'; +const DEFAULT_THEME_HC_LIGHT_SETTING_VALUE = 'Default High Contrast Light'; const DEFAULT_FILE_ICON_THEME_SETTING_VALUE = 'vs-seti'; @@ -58,10 +59,20 @@ const preferredLightThemeSettingSchema: IConfigurationPropertySchema = { enumItemLabels: colorThemeSettingEnumItemLabels, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; -const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { +const preferredHCDarkThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', - markdownDescription: nls.localize({ key: 'preferredHCColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme used in high contrast mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC), - default: DEFAULT_THEME_HC_SETTING_VALUE, + markdownDescription: nls.localize({ key: 'preferredHCDarkColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme used in high contrast dark mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC), + default: DEFAULT_THEME_HC_DARK_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + enumItemLabels: colorThemeSettingEnumItemLabels, + included: isWindows || isMacintosh, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const preferredHCLightThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + markdownDescription: nls.localize({ key: 'preferredHCLightColorTheme', comment: ['`#{0}#` will become a link to an other setting. Do not remove backtick or #'] }, 'Specifies the preferred color theme used in high contrast light mode when `#{0}#` is enabled.', ThemeSettings.DETECT_HC), + default: DEFAULT_THEME_HC_LIGHT_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, enumItemLabels: colorThemeSettingEnumItemLabels, @@ -106,7 +117,7 @@ const productIconThemeSettingSchema: IConfigurationPropertySchema = { const detectHCSchemeSettingSchema: IConfigurationPropertySchema = { type: 'boolean', default: true, - markdownDescription: nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme. The high contrast theme to use is specified by `#{0}#`", ThemeSettings.PREFERRED_HC_THEME), + markdownDescription: nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme. The high contrast theme to use is specified by `#{0}#` and `#{1}#`", ThemeSettings.PREFERRED_HC_DARK_THEME, ThemeSettings.PREFERRED_HC_LIGHT_THEME), scope: ConfigurationScope.APPLICATION }; @@ -118,7 +129,8 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.COLOR_THEME]: colorThemeSettingSchema, [ThemeSettings.PREFERRED_DARK_THEME]: preferredDarkThemeSettingSchema, [ThemeSettings.PREFERRED_LIGHT_THEME]: preferredLightThemeSettingSchema, - [ThemeSettings.PREFERRED_HC_THEME]: preferredHCThemeSettingSchema, + [ThemeSettings.PREFERRED_HC_DARK_THEME]: preferredHCDarkThemeSettingSchema, + [ThemeSettings.PREFERRED_HC_LIGHT_THEME]: preferredHCLightThemeSettingSchema, [ThemeSettings.FILE_ICON_THEME]: fileIconThemeSettingSchema, [ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema, [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index 5382bea8499..b7281cd238e 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import * as resources from 'vs/base/common/resources'; import { ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ExtensionData, IThemeExtensionPoint, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, VS_HC_LIGHT_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; @@ -32,8 +32,8 @@ export function registerColorThemeExtensionPoint() { type: 'string' }, uiTheme: { - description: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: \'vs\' is the light color theme, \'vs-dark\' is the dark color theme. \'hc-black\' is the dark high contrast theme.'), - enum: [VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME] + description: nls.localize('vscode.extension.contributes.themes.uiTheme', 'Base theme defining the colors around the editor: \'vs\' is the light color theme, \'vs-dark\' is the dark color theme. \'hc-black\' is the dark high contrast theme, \'hc-light\' is the light high contrast theme.'), + enum: [VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, VS_HC_LIGHT_THEME] }, path: { description: nls.localize('vscode.extension.contributes.themes.path', 'Path of the tmTheme file. The path is relative to the extension folder and is typically \'./colorthemes/awesome-color-theme.json\'.'), diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 69cc4d515cc..9d7bc2a6edd 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -16,8 +16,7 @@ export const IWorkbenchThemeService = refineServiceDecorator()); readonly onDidChangeEncoding = this._onDidChangeEncoding.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; @@ -257,6 +260,11 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt async save(options?: ISaveOptions): Promise { const target = await this.textFileService.save(this.resource, options); + // Emit as event + if (target) { + this._onDidSave.fire({ reason: options?.reason, source: options?.source }); + } + return !!target; } diff --git a/src/vs/workbench/services/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts index 7ca5d8fe698..81b80f081ad 100644 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ b/src/vs/workbench/services/views/browser/treeViewsService.ts @@ -5,9 +5,10 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITreeDataTransfer, ITreeItem } from 'vs/workbench/common/views'; +import { IDataTransfer } from 'vs/workbench/common/dnd'; +import { ITreeItem } from 'vs/workbench/common/views'; import { ITreeViewsService as ITreeViewsServiceCommon, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; -export interface ITreeViewsService extends ITreeViewsServiceCommon { } +export interface ITreeViewsService extends ITreeViewsServiceCommon { } export const ITreeViewsService = createDecorator('treeViewsService'); registerSingleton(ITreeViewsService, TreeviewsService); diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 5241c249fc6..9f24ddf4903 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -511,6 +511,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._cachedViewPositionsValue = this.getStoredCachedViewPositionsValue(); const newCachedPositions = this.getCachedViewPositions(); + const viewsToMove: { views: IViewDescriptor[]; from: ViewContainer; to: ViewContainer }[] = []; for (let viewId of newCachedPositions.keys()) { const viewDescriptor = this.getViewDescriptorById(viewId); @@ -533,7 +534,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor if (prevViewContainer && newViewContainer && newViewContainer !== prevViewContainer) { const viewDescriptor = this.getViewDescriptorById(viewId); if (viewDescriptor) { - this.moveViews([viewDescriptor], prevViewContainer, newViewContainer); + viewsToMove.push({ views: [viewDescriptor], from: prevViewContainer, to: newViewContainer }); } } } @@ -546,28 +547,29 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const currentContainer = this.getViewContainerByViewId(viewDescriptor.id); const defaultContainer = this.getDefaultContainerById(viewDescriptor.id); if (currentContainer && defaultContainer && currentContainer !== defaultContainer) { - this.moveViews([viewDescriptor], currentContainer, defaultContainer); + viewsToMove.push({ views: [viewDescriptor], from: currentContainer, to: defaultContainer }); } - - this.cachedViewInfo.delete(viewDescriptor.id); } }); }); - this.cachedViewInfo = this.getCachedViewPositions(); + this.cachedViewInfo = newCachedPositions; + for (const { views, from, to } of viewsToMove) { + this.moveViews(views, from, to); + } } - if (e.key === ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS && e.scope === StorageScope.GLOBAL && this.cachedViewContainerLocationsValue !== this.getStoredCachedViewContainerLocationsValue() /* This checks if current window changed the value or not */) { this._cachedViewContainerLocationsValue = this.getStoredCachedViewContainerLocationsValue(); const newCachedLocations = this.getCachedViewContainerLocations(); + const viewContainersToMove: [ViewContainer, ViewContainerLocation][] = []; for (const [containerId, location] of newCachedLocations.entries()) { const container = this.getViewContainerById(containerId); if (container) { if (location !== this.getViewContainerLocation(container)) { - this.moveViewContainerToLocation(container, location); + viewContainersToMove.push([container, location]); } } } @@ -578,12 +580,15 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor const defaultLocation = this.getDefaultViewContainerLocation(viewContainer); if (currentLocation !== defaultLocation) { - this.moveViewContainerToLocation(viewContainer, defaultLocation); + viewContainersToMove.push([viewContainer, defaultLocation]); } } }); this.cachedViewContainerInfo = this.getCachedViewContainerLocations(); + for (const [container, location] of viewContainersToMove) { + this.moveViewContainerToLocation(container, location); + } } } diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 21fbe13df29..00965871db3 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -11,11 +11,11 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -import { move } from 'vs/base/common/arrays'; +import { coalesce, move } from 'vs/base/common/arrays'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { isEqual } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { groupBy } from 'vs/base/common/collections'; +import { IStringDictionary } from 'vs/base/common/collections'; export function getViewsStateStorageId(viewContainerStorageId: string): string { return `${viewContainerStorageId}.hidden`; } @@ -109,9 +109,9 @@ class ViewDescriptorsState extends Disposable { } private updateWorkspaceState(viewDescriptors: ReadonlyArray): void { - const storedViewsStates: { [id: string]: IStoredWorkspaceViewState } = JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + const storedViewsStates = this.getStoredWorkspaceState(); for (const viewDescriptor of viewDescriptors) { - const viewState = this.state.get(viewDescriptor.id); + const viewState = this.get(viewDescriptor.id); if (viewState) { storedViewsStates[viewDescriptor.id] = { collapsed: !!viewState.collapsed, @@ -132,7 +132,7 @@ class ViewDescriptorsState extends Disposable { private updateGlobalState(viewDescriptors: ReadonlyArray): void { const storedGlobalState = this.getStoredGlobalState(); for (const viewDescriptor of viewDescriptors) { - const state = this.state.get(viewDescriptor.id); + const state = this.get(viewDescriptor.id); storedGlobalState.set(viewDescriptor.id, { id: viewDescriptor.id, isHidden: state && viewDescriptor.canToggleVisibility ? !state.visibleGlobal : false, @@ -147,13 +147,24 @@ class ViewDescriptorsState extends Disposable { && this.globalViewsStatesValue !== this.getStoredGlobalViewsStatesValue() /* This checks if current window changed the value or not */) { this._globalViewsStatesValue = undefined; const storedViewsVisibilityStates = this.getStoredGlobalState(); + const storedWorkspaceViewsStates = this.getStoredWorkspaceState(); const changedStates: { id: string; visible: boolean }[] = []; for (const [id, storedState] of storedViewsVisibilityStates) { - const state = this.state.get(id); + const state = this.get(id); if (state) { if (state.visibleGlobal !== !storedState.isHidden) { changedStates.push({ id, visible: !storedState.isHidden }); } + } else { + const workspaceViewState = storedWorkspaceViewsStates[id]; + this.set(id, { + active: false, + visibleGlobal: !storedState.isHidden, + visibleWorkspace: isUndefined(workspaceViewState?.isHidden) ? undefined : !workspaceViewState?.isHidden, + collapsed: workspaceViewState?.collapsed, + order: workspaceViewState?.order, + size: workspaceViewState?.size, + }); } } if (changedStates.length) { @@ -164,7 +175,7 @@ class ViewDescriptorsState extends Disposable { private initialize(): Map { const viewStates = new Map(); - const workspaceViewsStates = <{ [id: string]: IStoredWorkspaceViewState }>JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + const workspaceViewsStates = this.getStoredWorkspaceState(); for (const id of Object.keys(workspaceViewsStates)) { const workspaceViewState = workspaceViewsStates[id]; viewStates.set(id, { @@ -224,6 +235,10 @@ class ViewDescriptorsState extends Disposable { return viewStates; } + private getStoredWorkspaceState(): IStringDictionary { + return JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + } + private getStoredGlobalState(): Map { return this.parseStoredGlobalState(this.globalViewsStatesValue).state; } @@ -389,61 +404,61 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode return this.isViewDescriptorVisible(viewDescriptorItem); } - setVisible(id: string, visible: boolean, size?: number): void { - this.updateVisibility([{ id, visible, size }]); + setVisible(id: string, visible: boolean): void { + this.updateVisibility([{ id, visible }]); } - private updateVisibility(viewDescriptors: { id: string; visible: boolean; size?: number }[]): void { - const { toBeAdded, toBeRemoved } = groupBy(viewDescriptors, viewDescriptor => viewDescriptor.visible ? 'toBeAdded' : 'toBeRemoved'); - - const updateVisibility = (viewDescriptors: { id: string; visible: boolean; size?: number }[]): { viewDescriptorItem: IViewDescriptorItem; visibleIndex: number }[] => { - const result: { viewDescriptorItem: IViewDescriptorItem; visibleIndex: number }[] = []; - for (const { id, visible, size } of viewDescriptors) { - const foundViewDescriptor = this.findAndIgnoreIfNotFound(id); - if (!foundViewDescriptor) { - continue; - } - - const { viewDescriptorItem, visibleIndex } = foundViewDescriptor; - const viewDescriptor = viewDescriptorItem.viewDescriptor; - - if (!viewDescriptor.canToggleVisibility) { - continue; - } - - if (this.isViewDescriptorVisibleWhenActive(viewDescriptorItem) === visible) { - continue; - } - - if (viewDescriptor.workspace) { - viewDescriptorItem.state.visibleWorkspace = visible; - } else { - viewDescriptorItem.state.visibleGlobal = visible; - } - - if (typeof viewDescriptorItem.state.size === 'number') { - viewDescriptorItem.state.size = size; - } - - if (this.isViewDescriptorVisible(viewDescriptorItem) !== visible) { - // do not add events if visibility is not changed - continue; - } - - result.push({ viewDescriptorItem, visibleIndex }); + private updateVisibility(viewDescriptors: { id: string; visible: boolean }[]): void { + // First: Update and remove the view descriptors which are asked to be hidden + const viewDescriptorItemsToHide = coalesce(viewDescriptors.filter(({ visible }) => !visible) + .map(({ id }) => this.findAndIgnoreIfNotFound(id))); + const removed: IViewDescriptorRef[] = []; + for (const { viewDescriptorItem, visibleIndex } of viewDescriptorItemsToHide) { + if (this.updateViewDescriptorItemVisibility(viewDescriptorItem, false)) { + removed.push({ viewDescriptor: viewDescriptorItem.viewDescriptor, index: visibleIndex }); } - return result; - }; - - if (toBeRemoved?.length) { - const removedVisibleDescriptors = updateVisibility(toBeRemoved).map(({ viewDescriptorItem, visibleIndex }) => ({ viewDescriptor: viewDescriptorItem.viewDescriptor, index: visibleIndex })); - this.broadCastRemovedVisibleViewDescriptors(removedVisibleDescriptors); + } + if (removed.length) { + this.broadCastRemovedVisibleViewDescriptors(removed); } - if (toBeAdded?.length) { - const addedVisibleDescriptors = updateVisibility(toBeAdded).map(({ viewDescriptorItem, visibleIndex }) => ({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed })); - this.broadCastAddedVisibleViewDescriptors(addedVisibleDescriptors); + // Second: Update and add the view descriptors which are asked to be shown + const added: IAddedViewDescriptorRef[] = []; + for (const { id, visible } of viewDescriptors) { + if (!visible) { + continue; + } + const foundViewDescriptor = this.findAndIgnoreIfNotFound(id); + if (!foundViewDescriptor) { + continue; + } + const { viewDescriptorItem, visibleIndex } = foundViewDescriptor; + if (this.updateViewDescriptorItemVisibility(viewDescriptorItem, true)) { + added.push({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); + } } + if (added.length) { + this.broadCastAddedVisibleViewDescriptors(added); + } + } + + private updateViewDescriptorItemVisibility(viewDescriptorItem: IViewDescriptorItem, visible: boolean): boolean { + if (!viewDescriptorItem.viewDescriptor.canToggleVisibility) { + return false; + } + if (this.isViewDescriptorVisibleWhenActive(viewDescriptorItem) === visible) { + return false; + } + + // update visibility + if (viewDescriptorItem.viewDescriptor.workspace) { + viewDescriptorItem.state.visibleWorkspace = visible; + } else { + viewDescriptorItem.state.visibleGlobal = visible; + } + + // return `true` only if visibility is changed + return this.isViewDescriptorVisible(viewDescriptorItem) === visible; } isCollapsed(id: string): boolean { @@ -462,10 +477,12 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode return this.find(id).viewDescriptorItem.state.size; } - setSize(id: string, size: number): void { - const { viewDescriptorItem } = this.find(id); - if (viewDescriptorItem.state.size !== size) { - viewDescriptorItem.state.size = size; + setSizes(newSizes: readonly { id: string; size: number }[]): void { + for (const { id, size } of newSizes) { + const { viewDescriptorItem } = this.find(id); + if (viewDescriptorItem.state.size !== size) { + viewDescriptorItem.state.size = size; + } } this.viewDescriptorsState.updateState(this.allViewDescriptors); } diff --git a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts index ef47ed5bbb8..a08f79dfa63 100644 --- a/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts +++ b/src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts @@ -17,6 +17,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Event } from 'vs/base/common/event'; +import { getViewsStateStorageId } from 'vs/workbench/services/views/common/viewContainerModel'; const ViewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -580,4 +581,230 @@ suite('ViewContainerModel', () => { assert.strictEqual(target.elements.length, 0); }); + test('#142087: view descriptor visibility is not reset', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const viewDescriptor: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canToggleVisibility: true + }; + + storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ + id: viewDescriptor.id, + isHidden: true, + order: undefined + }]), StorageScope.GLOBAL, StorageTarget.USER); + + ViewsRegistry.registerViews([viewDescriptor], container); + + assert.strictEqual(testObject.isVisible(viewDescriptor.id), false); + assert.strictEqual(testObject.activeViewDescriptors[0].id, viewDescriptor.id); + assert.strictEqual(testObject.visibleViewDescriptors.length, 0); + }); + + test('remove event is triggered properly if mutliple views are hidden at the same time', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor1: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canToggleVisibility: true + }; + const viewDescriptor2: IViewDescriptor = { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canToggleVisibility: true + }; + const viewDescriptor3: IViewDescriptor = { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canToggleVisibility: true + }; + + ViewsRegistry.registerViews([viewDescriptor1, viewDescriptor2, viewDescriptor3], container); + + const remomveEvent = sinon.spy(); + testObject.onDidRemoveVisibleViewDescriptors(remomveEvent); + + const addEvent = sinon.spy(); + testObject.onDidAddVisibleViewDescriptors(addEvent); + + storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ + id: viewDescriptor1.id, + isHidden: false, + order: undefined + }, { + id: viewDescriptor2.id, + isHidden: true, + order: undefined + }, { + id: viewDescriptor3.id, + isHidden: true, + order: undefined + }]), StorageScope.GLOBAL, StorageTarget.USER); + + assert.ok(!addEvent.called, 'add event should not be called'); + assert.ok(remomveEvent.calledOnce, 'remove event should be called'); + assert.deepStrictEqual(remomveEvent.args[0][0], [{ + viewDescriptor: viewDescriptor3, + index: 2 + }, { + viewDescriptor: viewDescriptor2, + index: 1 + }]); + assert.strictEqual(target.elements.length, 1); + assert.strictEqual(target.elements[0].id, viewDescriptor1.id); + }); + + test('add event is triggered properly if mutliple views are hidden at the same time', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor1: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canToggleVisibility: true + }; + const viewDescriptor2: IViewDescriptor = { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canToggleVisibility: true + }; + const viewDescriptor3: IViewDescriptor = { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canToggleVisibility: true + }; + + ViewsRegistry.registerViews([viewDescriptor1, viewDescriptor2, viewDescriptor3], container); + testObject.setVisible(viewDescriptor1.id, false); + testObject.setVisible(viewDescriptor3.id, false); + + const removeEvent = sinon.spy(); + testObject.onDidRemoveVisibleViewDescriptors(removeEvent); + + const addEvent = sinon.spy(); + testObject.onDidAddVisibleViewDescriptors(addEvent); + + storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ + id: viewDescriptor1.id, + isHidden: false, + order: undefined + }, { + id: viewDescriptor2.id, + isHidden: false, + order: undefined + }, { + id: viewDescriptor3.id, + isHidden: false, + order: undefined + }]), StorageScope.GLOBAL, StorageTarget.USER); + + assert.ok(!removeEvent.called, 'remove event should not be called'); + + assert.ok(addEvent.calledOnce, 'add event should be called once'); + assert.deepStrictEqual(addEvent.args[0][0], [{ + viewDescriptor: viewDescriptor1, + index: 0, + collapsed: false, + size: undefined + }, { + viewDescriptor: viewDescriptor3, + index: 2, + collapsed: false, + size: undefined + }]); + + assert.strictEqual(target.elements.length, 3); + assert.strictEqual(target.elements[0].id, viewDescriptor1.id); + assert.strictEqual(target.elements[1].id, viewDescriptor2.id); + assert.strictEqual(target.elements[2].id, viewDescriptor3.id); + }); + + test('add and remove events are triggered properly if mutliple views are hidden and added at the same time', async function () { + container = ViewContainerRegistry.registerViewContainer({ id: 'test', title: 'test', ctorDescriptor: new SyncDescriptor({}) }, ViewContainerLocation.Sidebar); + const testObject = viewDescriptorService.getViewContainerModel(container); + const target = disposableStore.add(new ViewDescriptorSequence(testObject)); + const viewDescriptor1: IViewDescriptor = { + id: 'view1', + ctorDescriptor: null!, + name: 'Test View 1', + canToggleVisibility: true + }; + const viewDescriptor2: IViewDescriptor = { + id: 'view2', + ctorDescriptor: null!, + name: 'Test View 2', + canToggleVisibility: true + }; + const viewDescriptor3: IViewDescriptor = { + id: 'view3', + ctorDescriptor: null!, + name: 'Test View 3', + canToggleVisibility: true + }; + const viewDescriptor4: IViewDescriptor = { + id: 'view4', + ctorDescriptor: null!, + name: 'Test View 4', + canToggleVisibility: true + }; + + ViewsRegistry.registerViews([viewDescriptor1, viewDescriptor2, viewDescriptor3, viewDescriptor4], container); + testObject.setVisible(viewDescriptor1.id, false); + + const removeEvent = sinon.spy(); + testObject.onDidRemoveVisibleViewDescriptors(removeEvent); + + const addEvent = sinon.spy(); + testObject.onDidAddVisibleViewDescriptors(addEvent); + + storageService.store(getViewsStateStorageId('test.state'), JSON.stringify([{ + id: viewDescriptor1.id, + isHidden: false, + order: undefined + }, { + id: viewDescriptor2.id, + isHidden: true, + order: undefined + }, { + id: viewDescriptor3.id, + isHidden: false, + order: undefined + }, { + id: viewDescriptor4.id, + isHidden: true, + order: undefined + }]), StorageScope.GLOBAL, StorageTarget.USER); + + assert.ok(removeEvent.calledOnce, 'remove event should be called once'); + assert.deepStrictEqual(removeEvent.args[0][0], [{ + viewDescriptor: viewDescriptor4, + index: 2 + }, { + viewDescriptor: viewDescriptor2, + index: 0 + }]); + + assert.ok(addEvent.calledOnce, 'add event should be called once'); + assert.deepStrictEqual(addEvent.args[0][0], [{ + viewDescriptor: viewDescriptor1, + index: 0, + collapsed: false, + size: undefined + }]); + assert.strictEqual(target.elements.length, 2); + assert.strictEqual(target.elements[0].id, viewDescriptor1.id); + assert.strictEqual(target.elements[1].id, viewDescriptor3.id); + }); + }); diff --git a/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts new file mode 100644 index 00000000000..2ada834a567 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyHistoryService.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { SaveSource } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WorkingCopyHistoryModel, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; + +class BrowserWorkingCopyHistoryModel extends WorkingCopyHistoryModel { + + override async addEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise { + const entry = await super.addEntry(source, timestamp, token); + if (!token.isCancellationRequested) { + await this.store(token); // need to store on each add because we do not have long running shutdown support in web + } + + return entry; + } + + override async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise { + await super.updateEntry(entry, properties, token); + if (!token.isCancellationRequested) { + await this.store(token); // need to store on each remove because we do not have long running shutdown support in web + } + } + + override async removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise { + const removed = await super.removeEntry(entry, token); + if (removed && !token.isCancellationRequested) { + await this.store(token); // need to store on each remove because we do not have long running shutdown support in web + } + + return removed; + } +} + +export class BrowserWorkingCopyHistoryService extends WorkingCopyHistoryService { + + constructor( + @IFileService fileService: IFileService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @ILabelService labelService: ILabelService, + @ILogService logService: ILogService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); + } + + protected override createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { + return new BrowserWorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); + } +} + +// Register Service +registerSingleton(IWorkingCopyHistoryService, BrowserWorkingCopyHistoryService, true); diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 141dd0596bb..3e9c8ade776 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -67,7 +67,7 @@ export interface IFileWorkingCopyModel extends IDisposable { * Note: it is expected that the model fires a `onDidChangeContent` event * as part of the update. * - * @param the contents to use for the model + * @param contents the contents to use for the model * @param token support for cancellation */ update(contents: VSBufferReadableStream, token: CancellationToken): Promise; diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 90623819487..55387453019 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -13,7 +13,7 @@ import { toLocalResource, joinPath, isEqual, basename, dirname } from 'vs/base/c import { URI } from 'vs/base/common/uri'; import { IFileDialogService, IDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; -import { ISaveOptions } from 'vs/workbench/common/editor'; +import { ISaveOptions, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -135,6 +135,9 @@ export class FileWorkingCopyManager>; + private static readonly FILE_WORKING_COPY_SAVE_CREATE_SOURCE = SaveSourceRegistry.registerSource('fileWorkingCopyCreate.source', localize('fileWorkingCopyCreate.source', "File Created")); + private static readonly FILE_WORKING_COPY_SAVE_REPLACE_SOURCE = SaveSourceRegistry.registerSource('fileWorkingCopyReplace.source', localize('fileWorkingCopyReplace.source', "File Replaced")); + readonly stored: IStoredFileWorkingCopyManager; readonly untitled: IUntitledFileWorkingCopyManager; @@ -405,6 +408,14 @@ export class FileWorkingCopyManager; abstract onDidChangeContent: Event; + abstract onDidSave: Event; abstract isDirty(): boolean; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index 4fa30f81ae8..1f7853a9435 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -10,7 +10,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { IWorkingCopyBackup, IWorkingCopyBackupMeta, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackup, IWorkingCopyBackupMeta, IWorkingCopySaveEvent, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; import { assertIsDefined } from 'vs/base/common/types'; @@ -99,7 +99,7 @@ export interface IStoredFileWorkingCopy e /** * An event for when a stored file working copy was saved successfully. */ - readonly onDidSave: Event; + readonly onDidSave: Event; /** * An event indicating that a stored file working copy save operation failed. @@ -255,6 +255,20 @@ interface IStoredFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { readonly orphaned: boolean; } +export interface IStoredFileWorkingCopySaveEvent extends IWorkingCopySaveEvent { + + /** + * The resolved stat from the save operation. + */ + readonly stat: IFileStatWithMetadata; +} + +export function isStoredFileWorkingCopySaveEvent(e: IWorkingCopySaveEvent): e is IStoredFileWorkingCopySaveEvent { + const candidate = e as IStoredFileWorkingCopySaveEvent; + + return !!candidate.stat; +} + export class StoredFileWorkingCopy extends ResourceWorkingCopy implements IStoredFileWorkingCopy { readonly capabilities: WorkingCopyCapabilities = WorkingCopyCapabilities.None; @@ -276,7 +290,7 @@ export class StoredFileWorkingCopy extend private readonly _onDidSaveError = this._register(new Emitter()); readonly onDidSaveError = this._onDidSaveError.event; - private readonly _onDidSave = this._register(new Emitter()); + private readonly _onDidSave = this._register(new Emitter()); readonly onDidSave = this._onDidSave.event; private readonly _onDidRevert = this._register(new Emitter()); @@ -864,7 +878,7 @@ export class StoredFileWorkingCopy extend await this.workingCopyFileService.runSaveParticipants(this, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); } } catch (error) { - this.logService.error(`[stored file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); + this.logService.error(`[stored file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(), this.typeId); } } @@ -958,11 +972,11 @@ export class StoredFileWorkingCopy extend this.setOrphaned(false); // Emit Save Event - this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT); + this._onDidSave.fire({ reason: options.reason, stat, source: options.source }); } private handleSaveError(error: Error, versionId: number, options: IStoredFileWorkingCopySaveOptions): void { - (options.ignoreErrorHandler ? this.logService.trace : this.logService.error)(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); + (options.ignoreErrorHandler ? this.logService.trace : this.logService.error)(`[stored file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(), this.typeId); // Return early if the save() call was made asking to // handle the save error itself. @@ -1178,7 +1192,7 @@ export class StoredFileWorkingCopy extend } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(true), this.typeId); + this.logService.trace(msg, this.resource.toString(), this.typeId); } //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 6e3e128939e..3fce10d67fa 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -5,8 +5,7 @@ import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; -import { SaveReason } from 'vs/workbench/common/editor'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions, IStoredFileWorkingCopySaveEvent as IBaseStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; import { FileChangesEvent, FileChangeType, FileOperation, IFileService, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent } from 'vs/platform/files/common/files'; @@ -105,17 +104,12 @@ export interface IStoredFileWorkingCopyManager): true | Promise; } -export interface IStoredFileWorkingCopySaveEvent { +export interface IStoredFileWorkingCopySaveEvent extends IBaseStoredFileWorkingCopySaveEvent { /** * The stored file working copy that was successfully saved. */ readonly workingCopy: IStoredFileWorkingCopy; - - /** - * The reason why the stored file working copy was saved. - */ - readonly reason: SaveReason; } export interface IStoredFileWorkingCopyManagerResolveOptions { @@ -629,7 +623,7 @@ export class StoredFileWorkingCopyManager workingCopyListeners.add(workingCopy.onDidChangeReadonly(() => this._onDidChangeReadonly.fire(workingCopy))); workingCopyListeners.add(workingCopy.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire(workingCopy))); workingCopyListeners.add(workingCopy.onDidSaveError(() => this._onDidSaveError.fire(workingCopy))); - workingCopyListeners.add(workingCopy.onDidSave(reason => this._onDidSave.fire({ workingCopy: workingCopy, reason }))); + workingCopyListeners.add(workingCopy.onDidSave(e => this._onDidSave.fire({ workingCopy, ...e }))); workingCopyListeners.add(workingCopy.onDidRevert(() => this._onDidRevert.fire(workingCopy))); // Keep for disposal diff --git a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts index b893485ed87..baa8d0587d4 100644 --- a/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/untitledFileWorkingCopy.ts @@ -5,7 +5,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; -import { IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyBackup, IWorkingCopySaveEvent, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; @@ -105,6 +105,9 @@ export class UntitledFileWorkingCopy ex private readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; @@ -263,10 +266,17 @@ export class UntitledFileWorkingCopy ex //#region Save - save(options?: ISaveOptions): Promise { + async save(options?: ISaveOptions): Promise { this.trace('[untitled file working copy] save()'); - return this.saveDelegate(this, options); + const result = await this.saveDelegate(this, options); + + // Emit Save Event + if (result) { + this._onDidSave.fire({ reason: options?.reason, source: options?.source }); + } + + return result; } //#endregion @@ -300,6 +310,6 @@ export class UntitledFileWorkingCopy ex } private trace(msg: string): void { - this.logService.trace(msg, this.resource.toString(true), this.typeId); + this.logService.trace(msg, this.resource.toString(), this.typeId); } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopy.ts b/src/vs/workbench/services/workingCopy/common/workingCopy.ts index a0f1298a504..d7e92c074d9 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopy.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { ISaveOptions, IRevertOptions, SaveReason, SaveSource } from 'vs/workbench/common/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -93,6 +93,19 @@ export interface IWorkingCopyIdentifier { readonly resource: URI; } +export interface IWorkingCopySaveEvent { + + /** + * The reason why the working copy was saved. + */ + readonly reason?: SaveReason; + + /** + * The source of the working copy save request. + */ + readonly source?: SaveSource; +} + /** * A working copy is an abstract concept to unify handling of * data that can be worked on (e.g. edited) in an editor. @@ -132,6 +145,12 @@ export interface IWorkingCopy extends IWorkingCopyIdentifier { */ readonly onDidChangeContent: Event; + /** + * Used by the workbench e.g. to track local history + * (unless this working copy is untitled). + */ + readonly onDidSave: Event; + //#endregion diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts index 0e48c9abf7b..a38e6f26bb2 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts @@ -108,7 +108,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { private onDidRegister(workingCopy: IWorkingCopy): void { if (this.suspended) { - this.logService.warn(`[backup tracker] suspended, ignoring register event`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.warn(`[backup tracker] suspended, ignoring register event`, workingCopy.resource.toString(), workingCopy.typeId); return; } @@ -124,7 +124,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Check suspended if (this.suspended) { - this.logService.warn(`[backup tracker] suspended, ignoring unregister event`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.warn(`[backup tracker] suspended, ignoring unregister event`, workingCopy.resource.toString(), workingCopy.typeId); return; } @@ -134,7 +134,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { private onDidChangeDirty(workingCopy: IWorkingCopy): void { if (this.suspended) { - this.logService.warn(`[backup tracker] suspended, ignoring dirty change event`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.warn(`[backup tracker] suspended, ignoring dirty change event`, workingCopy.resource.toString(), workingCopy.typeId); return; } @@ -153,7 +153,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Check suspended if (this.suspended) { - this.logService.warn(`[backup tracker] suspended, ignoring content change event`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.warn(`[backup tracker] suspended, ignoring content change event`, workingCopy.resource.toString(), workingCopy.typeId); return; } @@ -171,7 +171,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Clear any running backup operation this.cancelBackupOperation(workingCopy); - this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString(), workingCopy.typeId); // Schedule new backup const cts = new CancellationTokenSource(); @@ -182,7 +182,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Backup if dirty if (workingCopy.isDirty()) { - this.logService.trace(`[backup tracker] creating backup`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] creating backup`, workingCopy.resource.toString(), workingCopy.typeId); try { const backup = await workingCopy.backup(cts.token); @@ -191,7 +191,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { } if (workingCopy.isDirty()) { - this.logService.trace(`[backup tracker] storing backup`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] storing backup`, workingCopy.resource.toString(), workingCopy.typeId); await this.workingCopyBackupService.backup(workingCopy, backup.content, this.getContentVersion(workingCopy), backup.meta, cts.token); } @@ -209,7 +209,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Keep in map for disposal as needed this.pendingBackupOperations.set(workingCopy, toDisposable(() => { - this.logService.trace(`[backup tracker] clearing pending backup creation`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] clearing pending backup creation`, workingCopy.resource.toString(), workingCopy.typeId); cts.dispose(true); clearTimeout(handle); @@ -237,7 +237,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Schedule backup discard asap const cts = new CancellationTokenSource(); (async () => { - this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString(), workingCopy.typeId); // Discard backup try { @@ -255,7 +255,7 @@ export abstract class WorkingCopyBackupTracker extends Disposable { // Keep in map for disposal as needed this.pendingBackupOperations.set(workingCopy, toDisposable(() => { - this.logService.trace(`[backup tracker] clearing pending backup discard`, workingCopy.resource.toString(true), workingCopy.typeId); + this.logService.trace(`[backup tracker] clearing pending backup discard`, workingCopy.resource.toString(), workingCopy.typeId); cts.dispose(true); })); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts index c42bfc127e3..e75246b8b83 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant.ts @@ -24,6 +24,7 @@ export class WorkingCopyFileOperationParticipant extends Disposable { addFileOperationParticipant(participant: IWorkingCopyFileOperationParticipant): IDisposable { const remove = this.participants.push(participant); + return toDisposable(() => remove()); } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts new file mode 100644 index 00000000000..ac684fb2d92 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistory.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { SaveSource } from 'vs/workbench/common/editor'; + +export const IWorkingCopyHistoryService = createDecorator('workingCopyHistoryService'); + +export interface IWorkingCopyHistoryEvent { + + /** + * The entry this event is about. + */ + readonly entry: IWorkingCopyHistoryEntry; +} + +export interface IWorkingCopyHistoryEntry { + + /** + * Unique identifier of this entry for the working copy. + */ + readonly id: string; + + /** + * The associated working copy of this entry. + */ + readonly workingCopy: { + readonly resource: URI; + readonly name: string; + }; + + /** + * The location on disk of this history entry. + */ + readonly location: URI; + + /** + * The time when this history entry was created. + */ + timestamp: number; + + /** + * Associated source with the history entry. + */ + source: SaveSource; +} + +export interface IWorkingCopyHistoryEntryDescriptor { + + /** + * The associated resource of this history entry. + */ + readonly resource: URI; + + /** + * Optional associated timestamp to use for the + * history entry. If not provided, the current + * time will be used. + */ + readonly timestamp?: number; + + /** + * Optional source why the entry was added. + */ + readonly source?: SaveSource; +} + +export interface IWorkingCopyHistoryService { + + readonly _serviceBrand: undefined; + + /** + * An event when an entry is added to the history. + */ + onDidAddEntry: Event; + + /** + * An event when an entry is changed in the history. + */ + onDidChangeEntry: Event; + + /** + * An event when an entry is replaced in the history. + */ + onDidReplaceEntry: Event; + + /** + * An event when an entry is removed from the history. + */ + onDidRemoveEntry: Event; + + /** + * An event when entries are moved in history. + */ + onDidMoveEntries: Event; + + /** + * An event when all entries are removed from the history. + */ + onDidRemoveEntries: Event; + + /** + * Adds a new entry to the history for the given working copy + * with an optional associated descriptor. + */ + addEntry(descriptor: IWorkingCopyHistoryEntryDescriptor, token: CancellationToken): Promise; + + /** + * Updates an entry in the local history if found. + */ + updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise; + + /** + * Removes an entry from the local history if found. + */ + removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise; + + /** + * Gets all history entries for the provided resource. + */ + getEntries(resource: URI, token: CancellationToken): Promise; + + /** + * Returns all resources for which history entries exist. + */ + getAll(token: CancellationToken): Promise; + + /** + * Removes all entries from all of local history. + */ + removeAll(token: CancellationToken): Promise; +} + +/** + * A limit on how many I/O operations we allow to run in parallel. + * We do not want to spam the file system with too many requests + * at the same time, so we limit to a maximum degree of parallellism. + */ +export const MAX_PARALLEL_HISTORY_IO_OPS = 20; diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts new file mode 100644 index 00000000000..ac98822ceb8 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -0,0 +1,744 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from 'vs/base/common/event'; +import { assertIsDefined } from 'vs/base/common/types'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { WorkingCopyHistoryTracker } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryTracker'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryEntryDescriptor, IWorkingCopyHistoryEvent, IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { URI } from 'vs/base/common/uri'; +import { DeferredPromise, Limiter } from 'vs/base/common/async'; +import { dirname, extname, isEqual, joinPath } from 'vs/base/common/resources'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { hash } from 'vs/base/common/hash'; +import { indexOfPath, randomPath } from 'vs/base/common/extpath'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ResourceMap } from 'vs/base/common/map'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { SaveSource, SaveSourceRegistry } from 'vs/workbench/common/editor'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { lastOrDefault } from 'vs/base/common/arrays'; + +interface ISerializedWorkingCopyHistoryModel { + readonly version: number; + readonly resource: string; + readonly entries: ISerializedWorkingCopyHistoryModelEntry[]; +} + +interface ISerializedWorkingCopyHistoryModelEntry { + readonly id: string; + readonly timestamp: number; + readonly source?: SaveSource; +} + +export class WorkingCopyHistoryModel { + + static readonly ENTRIES_FILE = 'entries.json'; + + private static readonly FILE_SAVED_SOURCE = SaveSourceRegistry.registerSource('default.source', localize('default.source', "File Saved")); + + private static readonly SETTINGS = { + MAX_ENTRIES: 'workbench.localHistory.maxFileEntries', + MERGE_PERIOD: 'workbench.localHistory.mergePeriod' + }; + + private entries: IWorkingCopyHistoryEntry[] = []; + + private whenResolved: Promise | undefined = undefined; + + private workingCopyResource: URI | undefined = undefined; + private workingCopyName: string | undefined = undefined; + + private historyEntriesFolder: URI | undefined = undefined; + private historyEntriesListingFile: URI | undefined = undefined; + + private historyEntriesNameMatcher: RegExp | undefined = undefined; + + private shouldStore: boolean = false; + + constructor( + workingCopyResource: URI, + private readonly historyHome: URI, + private readonly entryAddedEmitter: Emitter, + private readonly entryChangedEmitter: Emitter, + private readonly entryReplacedEmitter: Emitter, + private readonly entryRemovedEmitter: Emitter, + private readonly fileService: IFileService, + private readonly labelService: ILabelService, + private readonly logService: ILogService, + private readonly configurationService: IConfigurationService + ) { + this.setWorkingCopy(workingCopyResource); + } + + private setWorkingCopy(workingCopyResource: URI): void { + + // Update working copy + this.workingCopyResource = workingCopyResource; + this.workingCopyName = this.labelService.getUriBasenameLabel(workingCopyResource); + + this.historyEntriesNameMatcher = new RegExp(`[A-Za-z0-9]{4}${extname(workingCopyResource)}`); + + // Update locations + this.historyEntriesFolder = this.toHistoryEntriesFolder(this.historyHome, workingCopyResource); + this.historyEntriesListingFile = joinPath(this.historyEntriesFolder, WorkingCopyHistoryModel.ENTRIES_FILE); + + // Reset entries and resolved cache + this.entries = []; + this.whenResolved = undefined; + } + + private toHistoryEntriesFolder(historyHome: URI, workingCopyResource: URI): URI { + return joinPath(historyHome, hash(workingCopyResource.toString()).toString(16)); + } + + async addEntry(source = WorkingCopyHistoryModel.FILE_SAVED_SOURCE, timestamp = Date.now(), token: CancellationToken): Promise { + let entryToReplace: IWorkingCopyHistoryEntry | undefined = undefined; + + // Figure out if the last entry should be replaced based + // on settings that can define a interval for when an + // entry is not added as new entry but should replace. + // However, when save source is different, never replace. + const lastEntry = lastOrDefault(this.entries); + if (lastEntry && lastEntry.source === source) { + const configuredReplaceInterval = this.configurationService.getValue(WorkingCopyHistoryModel.SETTINGS.MERGE_PERIOD, { resource: this.workingCopyResource }); + if (timestamp - lastEntry.timestamp <= (configuredReplaceInterval * 1000 /* convert to millies */)) { + entryToReplace = lastEntry; + } + } + + // Replace lastest entry in history + if (entryToReplace) { + return this.doReplaceEntry(entryToReplace, timestamp, token); + } + + // Add entry to history + else { + return this.doAddEntry(source, timestamp, token); + } + } + + private async doAddEntry(source: SaveSource, timestamp: number, token: CancellationToken): Promise { + const workingCopyResource = assertIsDefined(this.workingCopyResource); + const workingCopyName = assertIsDefined(this.workingCopyName); + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + + // Perform a fast clone operation with minimal overhead to a new random location + const id = `${randomPath(undefined, undefined, 4)}${extname(workingCopyResource)}`; + const location = joinPath(historyEntriesFolder, id); + await this.fileService.cloneFile(workingCopyResource, location); + + // Add to list of entries + const entry: IWorkingCopyHistoryEntry = { + id, + workingCopy: { resource: workingCopyResource, name: workingCopyName }, + location, + timestamp, + source + }; + this.entries.push(entry); + + // Mark as in need to be stored to disk + this.shouldStore = true; + + // Events + this.entryAddedEmitter.fire({ entry }); + + return entry; + } + + private async doReplaceEntry(entry: IWorkingCopyHistoryEntry, timestamp: number, token: CancellationToken): Promise { + const workingCopyResource = assertIsDefined(this.workingCopyResource); + + // Perform a fast clone operation with minimal overhead to the existing location + await this.fileService.cloneFile(workingCopyResource, entry.location); + + // Update entry + entry.timestamp = timestamp; + + // Mark as in need to be stored to disk + this.shouldStore = true; + + // Events + this.entryReplacedEmitter.fire({ entry }); + + return entry; + } + + async removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise { + + // Make sure to await resolving when removing entries + await this.resolveEntriesOnce(); + + if (token.isCancellationRequested) { + return false; + } + + const index = this.entries.indexOf(entry); + if (index === -1) { + return false; + } + + // Delete from disk + await this.deleteEntry(entry); + + // Remove from model + this.entries.splice(index, 1); + + // Mark as in need to be stored to disk + this.shouldStore = true; + + // Events + this.entryRemovedEmitter.fire({ entry }); + + return true; + } + + async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise { + + // Make sure to await resolving when updating entries + await this.resolveEntriesOnce(); + + if (token.isCancellationRequested) { + return; + } + + const index = this.entries.indexOf(entry); + if (index === -1) { + return; + } + + // Update entry + entry.source = properties.source; + + // Mark as in need to be stored to disk + this.shouldStore = true; + + // Events + this.entryChangedEmitter.fire({ entry }); + } + + async getEntries(): Promise { + + // Make sure to await resolving when all entries are asked for + await this.resolveEntriesOnce(); + + // Return as many entries as configured by user settings + const configuredMaxEntries = this.configurationService.getValue(WorkingCopyHistoryModel.SETTINGS.MAX_ENTRIES, { resource: this.workingCopyResource }); + if (this.entries.length > configuredMaxEntries) { + return this.entries.slice(this.entries.length - configuredMaxEntries); + } + + return this.entries; + } + + async hasEntries(skipResolve: boolean): Promise { + + // Make sure to await resolving unless explicitly skipped + if (!skipResolve) { + await this.resolveEntriesOnce(); + } + + return this.entries.length > 0; + } + + private resolveEntriesOnce(): Promise { + if (!this.whenResolved) { + this.whenResolved = this.doResolveEntries(); + } + + return this.whenResolved; + } + + private async doResolveEntries(): Promise { + + // Resolve from disk + const entries = await this.resolveEntriesFromDisk(); + + // We now need to merge our in-memory entries with the + // entries we have found on disk because it is possible + // that new entries have been added before the entries + // listing file was updated + for (const entry of this.entries) { + entries.set(entry.id, entry); + } + + // Set as entries, sorted by timestamp + this.entries = Array.from(entries.values()).sort((entryA, entryB) => entryA.timestamp - entryB.timestamp); + } + + private async resolveEntriesFromDisk(): Promise> { + const workingCopyResource = assertIsDefined(this.workingCopyResource); + const workingCopyName = assertIsDefined(this.workingCopyName); + + const [entryListing, entryStats] = await Promise.all([ + + // Resolve entries listing file + this.readEntriesFile(), + + // Resolve children of history folder + this.readEntriesFolder() + ]); + + // Add from raw folder children + const entries = new Map(); + if (entryStats) { + for (const entryStat of entryStats) { + entries.set(entryStat.name, { + id: entryStat.name, + workingCopy: { resource: workingCopyResource, name: workingCopyName }, + location: entryStat.resource, + timestamp: entryStat.mtime, + source: WorkingCopyHistoryModel.FILE_SAVED_SOURCE + }); + } + } + + // Update from listing (to have more specific metadata) + if (entryListing) { + for (const entry of entryListing.entries) { + const existingEntry = entries.get(entry.id); + if (existingEntry) { + entries.set(entry.id, { + ...existingEntry, + timestamp: entry.timestamp, + source: entry.source ?? existingEntry.source + }); + } + } + } + + return entries; + } + + async moveEntries(targetWorkingCopyResource: URI, source: SaveSource, token: CancellationToken): Promise { + + // Ensure model stored so that any pending data is flushed + await this.store(token); + + if (token.isCancellationRequested) { + return undefined; + } + + // Rename existing entries folder + const sourceHistoryEntriesFolder = assertIsDefined(this.historyEntriesFolder); + const targetHistoryFolder = this.toHistoryEntriesFolder(this.historyHome, targetWorkingCopyResource); + try { + await this.fileService.move(sourceHistoryEntriesFolder, targetHistoryFolder, true); + } catch (error) { + if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + this.traceError(error); + } + } + + // Update our associated working copy + this.setWorkingCopy(targetWorkingCopyResource); + + // Add entry for the move + await this.addEntry(source, undefined, token); + + // Store model again to updated location + await this.store(token); + } + + async store(token: CancellationToken): Promise { + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + + if (!this.shouldStore) { + return; // fast return to avoid disk access when nothing changed + } + + // Make sure to await resolving when persisting + await this.resolveEntriesOnce(); + + if (token.isCancellationRequested) { + return undefined; + } + + // Cleanup based on max-entries setting + await this.cleanUpEntries(); + + // Without entries, remove the history folder + if (this.entries.length === 0) { + try { + await this.fileService.del(historyEntriesFolder, { recursive: true }); + } catch (error) { + this.traceError(error); + } + } + + // If we still have entries, update the entries meta file + else { + await this.writeEntriesFile(); + } + + // Mark as being up to date on disk + this.shouldStore = false; + } + + private async cleanUpEntries(): Promise { + const configuredMaxEntries = this.configurationService.getValue(WorkingCopyHistoryModel.SETTINGS.MAX_ENTRIES, { resource: this.workingCopyResource }); + if (this.entries.length <= configuredMaxEntries) { + return; // nothing to cleanup + } + + const entriesToDelete = this.entries.slice(0, this.entries.length - configuredMaxEntries); + const entriesToKeep = this.entries.slice(this.entries.length - configuredMaxEntries); + + // Delete entries from disk as instructed + for (const entryToDelete of entriesToDelete) { + await this.deleteEntry(entryToDelete); + } + + // Make sure to update our in-memory model as well + // because it will be persisted right after + this.entries = entriesToKeep; + + // Events + for (const entry of entriesToDelete) { + this.entryRemovedEmitter.fire({ entry }); + } + } + + private async deleteEntry(entry: IWorkingCopyHistoryEntry): Promise { + try { + await this.fileService.del(entry.location); + } catch (error) { + this.traceError(error); + } + } + + private async writeEntriesFile(): Promise { + const workingCopyResource = assertIsDefined(this.workingCopyResource); + const historyEntriesListingFile = assertIsDefined(this.historyEntriesListingFile); + + const serializedModel: ISerializedWorkingCopyHistoryModel = { + version: 1, + resource: workingCopyResource.toString(), + entries: this.entries.map(entry => { + return { + id: entry.id, + source: entry.source !== WorkingCopyHistoryModel.FILE_SAVED_SOURCE ? entry.source : undefined, + timestamp: entry.timestamp + }; + }) + }; + + await this.fileService.writeFile(historyEntriesListingFile, VSBuffer.fromString(JSON.stringify(serializedModel))); + } + + private async readEntriesFile(): Promise { + const historyEntriesListingFile = assertIsDefined(this.historyEntriesListingFile); + + let serializedModel: ISerializedWorkingCopyHistoryModel | undefined = undefined; + try { + serializedModel = JSON.parse((await this.fileService.readFile(historyEntriesListingFile)).value.toString()); + } catch (error) { + if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + this.traceError(error); + } + } + + return serializedModel; + } + + private async readEntriesFolder(): Promise { + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + const historyEntriesNameMatcher = assertIsDefined(this.historyEntriesNameMatcher); + + let rawEntries: IFileStatWithMetadata[] | undefined = undefined; + + // Resolve children of folder on disk + try { + rawEntries = (await this.fileService.resolve(historyEntriesFolder, { resolveMetadata: true })).children; + } catch (error) { + if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { + this.traceError(error); + } + } + + if (!rawEntries) { + return undefined; + } + + // Skip entries that do not seem to have valid file name + return rawEntries.filter(entry => + !isEqual(entry.resource, this.historyEntriesListingFile) && // not the listings file + historyEntriesNameMatcher.test(entry.name) // matching our expected file pattern for entries + ); + } + + private traceError(error: Error): void { + this.logService.trace('[Working Copy History Service]', error); + } +} + +export abstract class WorkingCopyHistoryService extends Disposable implements IWorkingCopyHistoryService { + + private static readonly FILE_MOVED_SOURCE = SaveSourceRegistry.registerSource('moved.source', localize('moved.source', "File Moved")); + private static readonly FILE_RENAMED_SOURCE = SaveSourceRegistry.registerSource('renamed.source', localize('renamed.source', "File Renamed")); + + declare readonly _serviceBrand: undefined; + + protected readonly _onDidAddEntry = this._register(new Emitter()); + readonly onDidAddEntry = this._onDidAddEntry.event; + + protected readonly _onDidChangeEntry = this._register(new Emitter()); + readonly onDidChangeEntry = this._onDidChangeEntry.event; + + protected readonly _onDidReplaceEntry = this._register(new Emitter()); + readonly onDidReplaceEntry = this._onDidReplaceEntry.event; + + private readonly _onDidMoveEntries = this._register(new Emitter()); + readonly onDidMoveEntries = this._onDidMoveEntries.event; + + protected readonly _onDidRemoveEntry = this._register(new Emitter()); + readonly onDidRemoveEntry = this._onDidRemoveEntry.event; + + private readonly _onDidRemoveEntries = this._register(new Emitter()); + readonly onDidRemoveEntries = this._onDidRemoveEntries.event; + + private readonly localHistoryHome = new DeferredPromise(); + + protected readonly models = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + + constructor( + @IFileService protected readonly fileService: IFileService, + @IRemoteAgentService protected readonly remoteAgentService: IRemoteAgentService, + @IEnvironmentService protected readonly environmentService: IEnvironmentService, + @IUriIdentityService protected readonly uriIdentityService: IUriIdentityService, + @ILabelService protected readonly labelService: ILabelService, + @ILogService protected readonly logService: ILogService, + @IConfigurationService protected readonly configurationService: IConfigurationService + ) { + super(); + + this.resolveLocalHistoryHome(); + + this._register(this.fileService.onDidRunOperation(e => this.onDidRunFileOperation(e))); + } + + private async resolveLocalHistoryHome(): Promise { + let historyHome: URI | undefined = undefined; + + // Prefer history to be stored in the remote if we are connected to a remote + try { + const remoteEnv = await this.remoteAgentService.getEnvironment(); + if (remoteEnv) { + historyHome = remoteEnv.localHistoryHome; + } + } catch (error) { + this.logService.trace(error); // ignore and fallback to local + } + + // But fallback to local if there is no remote + if (!historyHome) { + historyHome = this.environmentService.localHistoryHome; + } + + this.localHistoryHome.complete(historyHome); + } + + private async onDidRunFileOperation(e: FileOperationEvent): Promise { + if (!e.isOperation(FileOperation.MOVE)) { + return; // only interested in move operations + } + + const source = e.resource; + const target = e.target.resource; + + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + for (const [resource, model] of this.models) { + if (!this.uriIdentityService.extUri.isEqualOrParent(resource, source)) { + continue; // model does not match moved resource + } + + + // Determine new resulting target resource + let targetResource: URI; + if (isEqual(source, resource)) { + targetResource = target; // file got moved + } else { + const index = indexOfPath(resource.path, source.path); + targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved + } + + // Figure out save source + let saveSource: SaveSource; + if (isEqual(dirname(resource), dirname(targetResource))) { + saveSource = WorkingCopyHistoryService.FILE_RENAMED_SOURCE; + } else { + saveSource = WorkingCopyHistoryService.FILE_MOVED_SOURCE; + } + + // Move entries to target queued + promises.push(limiter.queue(() => this.moveEntries(model, saveSource, resource, targetResource))); + } + + if (!promises.length) { + return; + } + + // Await move operations + await Promise.all(promises); + + // Events + this._onDidMoveEntries.fire(); + } + + private async moveEntries(model: WorkingCopyHistoryModel, source: SaveSource, sourceWorkingCopyResource: URI, targetWorkingCopyResource: URI): Promise { + + // Move to target via model + await model.moveEntries(targetWorkingCopyResource, source, CancellationToken.None); + + // Update model in our map + this.models.delete(sourceWorkingCopyResource); + this.models.set(targetWorkingCopyResource, model); + } + + async addEntry({ resource, source, timestamp }: IWorkingCopyHistoryEntryDescriptor, token: CancellationToken): Promise { + if (!this.fileService.hasProvider(resource)) { + return undefined; // we require the working copy resource to be file service accessible + } + + // Resolve history model for working copy + const model = await this.getModel(resource); + if (token.isCancellationRequested) { + return undefined; + } + + // Add to model + return model.addEntry(source, timestamp, token); + } + + async updateEntry(entry: IWorkingCopyHistoryEntry, properties: { source: SaveSource }, token: CancellationToken): Promise { + + // Resolve history model for working copy + const model = await this.getModel(entry.workingCopy.resource); + if (token.isCancellationRequested) { + return; + } + + // Rename in model + return model.updateEntry(entry, properties, token); + } + + async removeEntry(entry: IWorkingCopyHistoryEntry, token: CancellationToken): Promise { + + // Resolve history model for working copy + const model = await this.getModel(entry.workingCopy.resource); + if (token.isCancellationRequested) { + return false; + } + + // Remove from model + return model.removeEntry(entry, token); + } + + async removeAll(token: CancellationToken): Promise { + const historyHome = await this.localHistoryHome.p; + if (token.isCancellationRequested) { + return; + } + + // Clear models + this.models.clear(); + + // Remove from disk + await this.fileService.del(historyHome, { recursive: true }); + + // Events + this._onDidRemoveEntries.fire(); + } + + async getEntries(resource: URI, token: CancellationToken): Promise { + const model = await this.getModel(resource); + if (token.isCancellationRequested) { + return []; + } + + const entries = await model.getEntries(); + return entries ?? []; + } + + async getAll(token: CancellationToken): Promise { + const historyHome = await this.localHistoryHome.p; + if (token.isCancellationRequested) { + return []; + } + + const all = new ResourceMap(); + + // Fill in all known model resources (they might not have yet persisted to disk) + for (const [resource, model] of this.models) { + const hasInMemoryEntries = await model.hasEntries(true /* skip resolving because we resolve below from disk */); + if (hasInMemoryEntries) { + all.set(resource, true); + } + } + + // Resolve all other resources by iterating the history home folder + try { + const resolvedHistoryHome = await this.fileService.resolve(historyHome); + if (resolvedHistoryHome.children) { + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + for (const child of resolvedHistoryHome.children) { + promises.push(limiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + try { + const serializedModel: ISerializedWorkingCopyHistoryModel = JSON.parse((await this.fileService.readFile(joinPath(child.resource, WorkingCopyHistoryModel.ENTRIES_FILE))).value.toString()); + if (serializedModel.entries.length > 0) { + all.set(URI.parse(serializedModel.resource), true); + } + } catch (error) { + // ignore - model might be missing or corrupt, but we need it + } + })); + } + + await Promise.all(promises); + } + } catch (error) { + // ignore - history might be entirely empty + } + + return Array.from(all.keys()); + } + + private async getModel(resource: URI): Promise { + const historyHome = await this.localHistoryHome.p; + + let model = this.models.get(resource); + if (!model) { + model = this.createModel(resource, historyHome); + this.models.set(resource, model); + } + + return model; + } + + protected createModel(resource: URI, historyHome: URI): WorkingCopyHistoryModel { + return new WorkingCopyHistoryModel(resource, historyHome, this._onDidAddEntry, this._onDidChangeEntry, this._onDidReplaceEntry, this._onDidRemoveEntry, this.fileService, this.labelService, this.logService, this.configurationService); + } +} + +// Register History Tracker +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkingCopyHistoryTracker, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts new file mode 100644 index 00000000000..fd65ce4a595 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryTracker.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IdleValue, Limiter } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { SaveSource, SaveSourceRegistry } from 'vs/workbench/common/editor'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { isStoredFileWorkingCopySaveEvent, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { IWorkingCopySaveEvent, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { Schemas } from 'vs/base/common/network'; +import { ResourceGlobMatcher } from 'vs/workbench/common/resources'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; + +export class WorkingCopyHistoryTracker extends Disposable implements IWorkbenchContribution { + + private static readonly SETTINGS = { + ENABLED: 'workbench.localHistory.enabled', + SIZE_LIMIT: 'workbench.localHistory.maxFileSize', + EXCLUDES: 'workbench.localHistory.exclude' + }; + + private static readonly UNDO_REDO_SAVE_SOURCE = SaveSourceRegistry.registerSource('undoRedo.source', localize('undoRedo.source', "Undo / Redo")); + + private readonly limiter = this._register(new Limiter(MAX_PARALLEL_HISTORY_IO_OPS)); + + private readonly resourceExcludeMatcher = this._register(new IdleValue(() => { + const matcher = this._register(new ResourceGlobMatcher( + root => this.configurationService.getValue(WorkingCopyHistoryTracker.SETTINGS.EXCLUDES, { resource: root }), + event => event.affectsConfiguration(WorkingCopyHistoryTracker.SETTINGS.EXCLUDES), + this.contextService, + this.configurationService + )); + + return matcher; + })); + + private readonly pendingAddHistoryEntryOperations = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + + private readonly workingCopyContentVersion = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + private readonly historyEntryContentVersion = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + + constructor( + @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, + @IWorkingCopyHistoryService private readonly workingCopyHistoryService: IWorkingCopyHistoryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IPathService private readonly pathService: IPathService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUndoRedoService private readonly undoRedoService: IUndoRedoService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners() { + + // Working Copy events + this._register(this.workingCopyService.onDidChangeContent(workingCopy => this.onDidChangeContent(workingCopy))); + this._register(this.workingCopyService.onDidSave(e => this.onDidSave(e))); + } + + private onDidChangeContent(workingCopy: IWorkingCopy): void { + + // Increment content version ID for resource + const contentVersionId = this.getContentVersion(workingCopy); + this.workingCopyContentVersion.set(workingCopy.resource, contentVersionId + 1); + } + + private getContentVersion(workingCopy: IWorkingCopy): number { + return this.workingCopyContentVersion.get(workingCopy.resource) || 0; + } + + private onDidSave(e: IWorkingCopySaveEvent): void { + if (!this.shouldTrackHistory(e)) { + return; // return early for working copies we are not interested in + } + + const contentVersion = this.getContentVersion(e.workingCopy); + if (this.historyEntryContentVersion.get(e.workingCopy.resource) === contentVersion) { + return; // return early when content version already has associated history entry + } + + // Cancel any previous operation for this resource + this.pendingAddHistoryEntryOperations.get(e.workingCopy.resource)?.dispose(true); + + // Create new cancellation token support and remember + const cts = new CancellationTokenSource(); + this.pendingAddHistoryEntryOperations.set(e.workingCopy.resource, cts); + + // Queue new operation to add to history + this.limiter.queue(async () => { + if (cts.token.isCancellationRequested) { + return; + } + + const contentVersion = this.getContentVersion(e.workingCopy); + + // Figure out source of save operation if not provided already + let source = e.source; + if (!e.source) { + source = this.resolveSourceFromUndoRedo(e); + } + + // Add entry + await this.workingCopyHistoryService.addEntry({ resource: e.workingCopy.resource, source, timestamp: e.stat.mtime }, cts.token); + + // Remember content version as being added to history + this.historyEntryContentVersion.set(e.workingCopy.resource, contentVersion); + + if (cts.token.isCancellationRequested) { + return; + } + + // Finally remove from pending operations + this.pendingAddHistoryEntryOperations.delete(e.workingCopy.resource); + }); + } + + private resolveSourceFromUndoRedo(e: IWorkingCopySaveEvent): SaveSource | undefined { + const lastStackElement = this.undoRedoService.getLastElement(e.workingCopy.resource); + if (lastStackElement) { + if (lastStackElement.code === 'undoredo.textBufferEdit') { + return undefined; // ignore any unspecific stack element that resulted just from typing + } + + return lastStackElement.label; + } + + const allStackElements = this.undoRedoService.getElements(e.workingCopy.resource); + if (allStackElements.future.length > 0 || allStackElements.past.length > 0) { + return WorkingCopyHistoryTracker.UNDO_REDO_SAVE_SOURCE; + } + + return undefined; + } + + private shouldTrackHistory(e: IWorkingCopySaveEvent): e is IStoredFileWorkingCopySaveEvent { + if ( + e.workingCopy.resource.scheme !== this.pathService.defaultUriScheme && // track history for all workspace resources + e.workingCopy.resource.scheme !== Schemas.vscodeUserData // track history for all settings + ) { + return false; // do not support unknown resources + } + + if (!isStoredFileWorkingCopySaveEvent(e)) { + return false; // only support working copies that are backed by stored files + } + + const configuredMaxFileSizeInBytes = 1024 * this.configurationService.getValue(WorkingCopyHistoryTracker.SETTINGS.SIZE_LIMIT, { resource: e.workingCopy.resource }); + if (e.stat.size > configuredMaxFileSizeInBytes) { + return false; // only track files that are not too large + } + + if (this.configurationService.getValue(WorkingCopyHistoryTracker.SETTINGS.ENABLED, { resource: e.workingCopy.resource }) === false) { + return false; // do not track when history is disabled + } + + // Finally check for exclude setting + return !this.resourceExcludeMatcher.value.matches(e.workingCopy.resource); + } +} diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 39a2fa4797e..e62fcc1fb1a 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -9,10 +9,18 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { IWorkingCopy, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IWorkingCopy, IWorkingCopyIdentifier, IWorkingCopySaveEvent as IBaseWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/workingCopy'; export const IWorkingCopyService = createDecorator('workingCopyService'); +export interface IWorkingCopySaveEvent extends IBaseWorkingCopySaveEvent { + + /** + * The working copy that was saved. + */ + readonly workingCopy: IWorkingCopy; +} + export interface IWorkingCopyService { readonly _serviceBrand: undefined; @@ -40,6 +48,11 @@ export interface IWorkingCopyService { */ readonly onDidChangeContent: Event; + /** + * An event for when a working copy was saved. + */ + readonly onDidSave: Event; + //#endregion @@ -103,6 +116,12 @@ export interface IWorkingCopyService { */ get(identifier: IWorkingCopyIdentifier): IWorkingCopy | undefined; + /** + * Returns all working copies with the given resource or `undefined` + * if no such working copy exists. + */ + getAll(resource: URI): readonly IWorkingCopy[] | undefined; + //#endregion } @@ -124,6 +143,9 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + //#endregion @@ -137,7 +159,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { let workingCopiesForResource = this.mapResourceToWorkingCopies.get(workingCopy.resource); if (workingCopiesForResource?.has(workingCopy.typeId)) { - throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString(true)} and type ${workingCopy.typeId}.`); + throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString()} and type ${workingCopy.typeId}.`); } // Registry (all) @@ -154,6 +176,7 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic const disposables = new DisposableStore(); disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy))); disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); + disposables.add(workingCopy.onDidSave(e => this._onDidSave.fire({ workingCopy, ...e }))); // Send some initial events this._onDidRegister.fire(workingCopy); @@ -202,6 +225,15 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return this.mapResourceToWorkingCopies.get(identifier.resource)?.get(identifier.typeId); } + getAll(resource: URI): readonly IWorkingCopy[] | undefined { + const workingCopies = this.mapResourceToWorkingCopies.get(resource); + if (!workingCopies) { + return undefined; + } + + return Array.from(workingCopies.values()); + } + //#endregion diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts new file mode 100644 index 00000000000..36aae715453 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Limiter } from 'vs/base/common/async'; +import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; + +export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { + + constructor( + @IFileService fileService: IFileService, + @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @ILabelService labelService: ILabelService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @ILogService logService: ILogService, + @IConfigurationService configurationService: IConfigurationService + ) { + super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); + + this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + } + + private onWillShutdown(e: WillShutdownEvent): void { + + // Prolong shutdown for orderly model shutdown + e.join((async () => { + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + const models = Array.from(this.models.values()); + for (const model of models) { + promises.push(limiter.queue(async () => { + if (e.token.isCancellationRequested) { + return; + } + + try { + await model.store(e.token); + } catch (error) { + this.logService.trace(error); + } + })); + } + + await Promise.all(promises); + })(), 'join.workingCopyHistory'); + } +} + +// Register Service +registerSingleton(IWorkingCopyHistoryService, NativeWorkingCopyHistoryService, true); diff --git a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts index 5febd89c232..aa09a909665 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/resourceWorkingCopy.test.ts @@ -23,6 +23,7 @@ suite('ResourceWorkingCopy', function () { capabilities = WorkingCopyCapabilities.None; onDidChangeDirty = Event.None; onDidChangeContent = Event.None; + onDidSave = Event.None; isDirty(): boolean { return false; } async backup(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } async save(options?: ISaveOptions): Promise { return false; } diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index 853ac3a689b..d7985656404 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; +import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelContentChangedEvent, IStoredFileWorkingCopyModelFactory, isStoredFileWorkingCopySaveEvent, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, newWriteableBufferStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -14,7 +14,7 @@ import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { basename } from 'vs/base/common/resources'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; -import { SaveReason } from 'vs/workbench/common/editor'; +import { SaveReason, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { Promises } from 'vs/base/common/async'; import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; @@ -158,6 +158,11 @@ suite('StoredFileWorkingCopy', function () { contentChangeCounter++; }); + let savedCounter = 0; + workingCopy.onDidSave(() => { + savedCounter++; + }); + // Dirty from: Model content change workingCopy.model?.updateContents('hello dirty'); assert.strictEqual(contentChangeCounter, 1); @@ -171,6 +176,7 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(workingCopy.isDirty(), false); assert.strictEqual(workingCopy.hasState(StoredFileWorkingCopyState.DIRTY), false); assert.strictEqual(changeDirtyCounter, 2); + assert.strictEqual(savedCounter, 1); // Dirty from: Initial contents await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello dirty stream')) }); @@ -417,10 +423,10 @@ suite('StoredFileWorkingCopy', function () { test('save (no errors)', async () => { let savedCounter = 0; - let lastSavedReason: SaveReason | undefined = undefined; - workingCopy.onDidSave(reason => { + let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; + workingCopy.onDidSave(e => { savedCounter++; - lastSavedReason = reason; + lastSaveEvent = e; }); let saveErrorCounter = 0; @@ -441,17 +447,22 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(savedCounter, 1); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(lastSavedReason, SaveReason.EXPLICIT); + assert.strictEqual(lastSaveEvent!.reason, SaveReason.EXPLICIT); + assert.ok(lastSaveEvent!.stat); + assert.ok(isStoredFileWorkingCopySaveEvent(lastSaveEvent!)); assert.strictEqual(workingCopy.model?.pushedStackElement, true); // save reason workingCopy.model?.updateContents('hello save'); - await workingCopy.save({ reason: SaveReason.AUTO }); + + const source = SaveSourceRegistry.registerSource('testSource', 'Hello Save'); + await workingCopy.save({ reason: SaveReason.AUTO, source }); assert.strictEqual(savedCounter, 2); assert.strictEqual(saveErrorCounter, 0); assert.strictEqual(workingCopy.isDirty(), false); - assert.strictEqual(lastSavedReason, SaveReason.AUTO); + assert.strictEqual((lastSaveEvent as IStoredFileWorkingCopySaveEvent).reason, SaveReason.AUTO); + assert.strictEqual((lastSaveEvent as IStoredFileWorkingCopySaveEvent).source, source); // multiple saves in parallel are fine and result // in a single save when content does not change diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts index 6b2fc6c8c95..216c922bf5f 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopyManager.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { workbenchInstantiationService, TestServiceAccessor, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; -import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; +import { StoredFileWorkingCopyManager, IStoredFileWorkingCopyManager, IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager'; import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; @@ -305,8 +305,10 @@ suite('StoredFileWorkingCopyManager', () => { } }); - manager.onDidSave(({ workingCopy }) => { - if (workingCopy.resource.toString() === resource1.toString()) { + let lastSaveEvent: IStoredFileWorkingCopySaveEvent | undefined = undefined; + manager.onDidSave((e) => { + if (e.workingCopy.resource.toString() === resource1.toString()) { + lastSaveEvent = e; savedCounter++; } }); @@ -352,6 +354,8 @@ suite('StoredFileWorkingCopyManager', () => { assert.strictEqual(gotNonDirtyCounter, 2); assert.strictEqual(revertedCounter, 1); assert.strictEqual(savedCounter, 1); + assert.strictEqual(lastSaveEvent!.workingCopy, workingCopy1); + assert.ok(lastSaveEvent!.stat); assert.strictEqual(saveErrorCounter, 1); assert.strictEqual(createdCounter, 2); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 01896acf907..679b0ceff4a 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { URI } from 'vs/base/common/uri'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; -import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopySaveEvent, WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; suite('WorkingCopyService', () => { @@ -20,6 +20,9 @@ suite('WorkingCopyService', () => { const onDidChangeContent: IWorkingCopy[] = []; service.onDidChangeContent(copy => onDidChangeContent.push(copy)); + const onDidSave: IWorkingCopySaveEvent[] = []; + service.onDidSave(copy => onDidSave.push(copy)); + const onDidRegister: IWorkingCopy[] = []; service.onDidRegister(copy => onDidRegister.push(copy)); @@ -36,6 +39,7 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.has(resource1), false); assert.strictEqual(service.has({ resource: resource1, typeId: 'testWorkingCopyType' }), false); assert.strictEqual(service.get({ resource: resource1, typeId: 'testWorkingCopyType' }), undefined); + assert.strictEqual(service.getAll(resource1), undefined); const copy1 = new TestWorkingCopy(resource1); const unregister1 = service.registerWorkingCopy(copy1); @@ -50,7 +54,12 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.get(copy1), copy1); assert.strictEqual(service.hasDirty, false); + const copies = service.getAll(copy1.resource); + assert.strictEqual(copies?.length, 1); + assert.strictEqual(copies[0], copy1); + copy1.setDirty(true); + copy1.save(); assert.strictEqual(copy1.isDirty(), true); assert.strictEqual(service.dirtyCount, 1); @@ -62,6 +71,8 @@ suite('WorkingCopyService', () => { assert.strictEqual(service.hasDirty, true); assert.strictEqual(onDidChangeDirty.length, 1); assert.strictEqual(onDidChangeDirty[0], copy1); + assert.strictEqual(onDidSave.length, 1); + assert.strictEqual(onDidSave[0].workingCopy, copy1); copy1.setContent('foo'); @@ -142,6 +153,12 @@ suite('WorkingCopyService', () => { const copy3 = new TestWorkingCopy(resource, false, typeId3); const dispose3 = service.registerWorkingCopy(copy3); + const copies = service.getAll(resource); + assert.strictEqual(copies?.length, 3); + assert.strictEqual(copies[0], copy1); + assert.strictEqual(copies[1], copy2); + assert.strictEqual(copies[2], copy3); + assert.strictEqual(service.dirtyCount, 0); assert.strictEqual(service.isDirty(resource), false); assert.strictEqual(service.isDirty(resource, typeId1), false); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts index fa7709542c9..a0c22c68010 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -61,7 +61,7 @@ export class NodeTestWorkingCopyBackupService extends NativeWorkingCopyBackupSer this.diskFileSystemProvider = new DiskFileSystemProvider(logService); fileService.registerProvider(Schemas.file, this.diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, this.diskFileSystemProvider, Schemas.userData, logService)); + fileService.registerProvider(Schemas.vscodeUserData, new FileUserDataProvider(Schemas.file, this.diskFileSystemProvider, Schemas.vscodeUserData, logService)); this.fileService = fileService; this.backupResourceJoiners = []; @@ -275,13 +275,13 @@ flakySuite('WorkingCopyBackupService', () => { // No Type ID let backupId = toUntypedWorkingCopyId(backupResource); let filePathHash = hashIdentifier(backupId); - let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString(); + let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); // With Type ID backupId = toTypedWorkingCopyId(backupResource); filePathHash = hashIdentifier(backupId); - expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString(); + expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); }); @@ -294,13 +294,13 @@ flakySuite('WorkingCopyBackupService', () => { // No Type ID let backupId = toUntypedWorkingCopyId(backupResource); let filePathHash = hashIdentifier(backupId); - let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString(); + let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); // With Type ID backupId = toTypedWorkingCopyId(backupResource); filePathHash = hashIdentifier(backupId); - expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString(); + expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); }); @@ -313,13 +313,13 @@ flakySuite('WorkingCopyBackupService', () => { // No Type ID let backupId = toUntypedWorkingCopyId(backupResource); let filePathHash = hashIdentifier(backupId); - let expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.userData }).toString(); + let expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); // With Type ID backupId = toTypedWorkingCopyId(backupResource); filePathHash = hashIdentifier(backupId); - expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.userData }).toString(); + expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.vscodeUserData }).toString(); assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); }); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts new file mode 100644 index 00000000000..397264e7081 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts @@ -0,0 +1,794 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Event } from 'vs/base/common/event'; +import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { TestNativePathService, TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { TestContextService, TestProductService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { Schemas } from 'vs/base/common/network'; +import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { dirname, join } from 'vs/base/common/path'; +import { Promises } from 'vs/base/node/pfs'; +import { URI } from 'vs/base/common/uri'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { TestRemoteAgentService } from 'vs/workbench/services/remote/test/common/testServices'; +import { existsSync, readFileSync, unlinkSync } from 'fs'; +import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryEntryDescriptor, IWorkingCopyHistoryEvent } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { IFileService } from 'vs/platform/files/common/files'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { LabelService } from 'vs/workbench/services/label/common/labelService'; +import { TestLifecycleService, TestWillShutdownEvent } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { NativeWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; +import { joinPath, dirname as resourcesDirname, basename } from 'vs/base/common/resources'; +import { firstOrDefault } from 'vs/base/common/arrays'; + +class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { + + constructor(private readonly testDir: string) { + super({ ...TestNativeWindowConfiguration, 'user-data-dir': testDir }, TestProductService); + } + + override get localHistoryHome() { + return joinPath(URI.file(this.testDir), 'History'); + } +} + +export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryService { + + readonly _fileService: IFileService; + readonly _configurationService: TestConfigurationService; + readonly _lifecycleService: TestLifecycleService; + + constructor(testDir: string) { + const environmentService = new TestWorkbenchEnvironmentService(testDir); + const logService = new NullLogService(); + const fileService = new FileService(logService); + + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + + const remoteAgentService = new TestRemoteAgentService(); + + const uriIdentityService = new UriIdentityService(fileService); + + const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService()); + + const lifecycleService = new TestLifecycleService(); + + const configurationService = new TestConfigurationService(); + + super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, lifecycleService, logService, configurationService); + + this._fileService = fileService; + this._configurationService = configurationService; + this._lifecycleService = lifecycleService; + } +} + +flakySuite('WorkingCopyHistoryService', () => { + + let testDir: string; + let historyHome: string; + let workHome: string; + let service: TestWorkingCopyHistoryService; + + let testFile1Path: string; + let testFile2Path: string; + let testFile3Path: string; + + const testFile1PathContents = 'Hello Foo'; + const testFile2PathContents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + const testFile3PathContents = 'Hello Bar'; + + setup(async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'workingcopyhistoryservice'); + historyHome = join(testDir, 'User', 'History'); + workHome = join(testDir, 'work'); + + service = new TestWorkingCopyHistoryService(testDir); + + await Promises.mkdir(historyHome, { recursive: true }); + await Promises.mkdir(workHome, { recursive: true }); + + testFile1Path = join(workHome, 'foo.txt'); + testFile2Path = join(workHome, 'bar.txt'); + testFile3Path = join(workHome, 'foo-bar.txt'); + + await Promises.writeFile(testFile1Path, testFile1PathContents); + await Promises.writeFile(testFile2Path, testFile2PathContents); + await Promises.writeFile(testFile3Path, testFile3PathContents); + }); + + let increasingTimestampCounter = 1; + + async function addEntry(descriptor: IWorkingCopyHistoryEntryDescriptor, token: CancellationToken, expectEntryAdded?: boolean): Promise; + async function addEntry(descriptor: IWorkingCopyHistoryEntryDescriptor, token: CancellationToken, expectEntryAdded: false): Promise; + async function addEntry(descriptor: IWorkingCopyHistoryEntryDescriptor, token: CancellationToken, expectEntryAdded = true): Promise { + const entry = await service.addEntry({ + ...descriptor, + timestamp: increasingTimestampCounter++ // very important to get tests to not be flaky with stable sort order + }, token); + + if (expectEntryAdded) { + assert.ok(entry, 'Unexpected undefined local history entry'); + assert.strictEqual(existsSync(entry.location.fsPath), true, 'Unexpected local history not stored on disk'); + } + + return entry; + } + + teardown(() => { + service.dispose(); + + return Promises.rm(testDir); + }); + + test('addEntry', async () => { + let addEvents: IWorkingCopyHistoryEvent[] = []; + service.onDidAddEntry(e => addEvents.push(e)); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + // Add Entry works + + const entry1A = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + const entry2A = await addEntry({ resource: workingCopy2.resource, source: 'My Source' }, CancellationToken.None); + + assert.strictEqual(readFileSync(entry1A.location.fsPath).toString(), testFile1PathContents); + assert.strictEqual(readFileSync(entry2A.location.fsPath).toString(), testFile2PathContents); + + assert.strictEqual(addEvents.length, 2); + assert.strictEqual(addEvents[0].entry.workingCopy.resource.toString(), workingCopy1.resource.toString()); + assert.strictEqual(addEvents[1].entry.workingCopy.resource.toString(), workingCopy2.resource.toString()); + assert.strictEqual(addEvents[1].entry.source, 'My Source'); + + const entry1B = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + const entry2B = await addEntry({ resource: workingCopy2.resource }, CancellationToken.None); + + assert.strictEqual(readFileSync(entry1B.location.fsPath).toString(), testFile1PathContents); + assert.strictEqual(readFileSync(entry2B.location.fsPath).toString(), testFile2PathContents); + + assert.strictEqual(addEvents.length, 4); + assert.strictEqual(addEvents[2].entry.workingCopy.resource.toString(), workingCopy1.resource.toString()); + assert.strictEqual(addEvents[3].entry.workingCopy.resource.toString(), workingCopy2.resource.toString()); + + // Cancellation works + + const cts = new CancellationTokenSource(); + const entry1CPromise = addEntry({ resource: workingCopy1.resource }, cts.token, false); + cts.dispose(true); + + const entry1C = await entry1CPromise; + assert.ok(!entry1C); + + assert.strictEqual(addEvents.length, 4); + + // Invalid working copies are ignored + + const workingCopy3 = new TestWorkingCopy(URI.file(testFile2Path).with({ scheme: 'unsupported' })); + const entry3A = await addEntry({ resource: workingCopy3.resource }, CancellationToken.None, false); + assert.ok(!entry3A); + + assert.strictEqual(addEvents.length, 4); + }); + + test('renameEntry', async () => { + let changeEvents: IWorkingCopyHistoryEvent[] = []; + service.onDidChangeEntry(e => changeEvents.push(e)); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource, source: 'My Source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + await service.updateEntry(entry, { source: 'Hello Rename' }, CancellationToken.None); + + assert.strictEqual(changeEvents.length, 1); + assert.strictEqual(changeEvents[0].entry, entry); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries[0].source, 'Hello Rename'); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + assert.strictEqual(entries[0].source, 'Hello Rename'); + }); + + test('removeEntry', async () => { + let removeEvents: IWorkingCopyHistoryEvent[] = []; + service.onDidRemoveEntry(e => removeEvents.push(e)); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource, source: 'My Source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + let removed = await service.removeEntry(entry2, CancellationToken.None); + assert.strictEqual(removed, true); + + assert.strictEqual(removeEvents.length, 1); + assert.strictEqual(removeEvents[0].entry, entry2); + + // Cannot remove same entry again + removed = await service.removeEntry(entry2, CancellationToken.None); + assert.strictEqual(removed, false); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + }); + + test('removeEntry - deletes history entries folder when last entry removed', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + let entry = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + + // Simulate shutdown + let event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + assert.strictEqual(existsSync(dirname(entry.location.fsPath)), true); + + entry = firstOrDefault(await service.getEntries(workingCopy1.resource, CancellationToken.None))!; + assert.ok(entry); + + await service.removeEntry(entry, CancellationToken.None); + + // Simulate shutdown + event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + assert.strictEqual(existsSync(dirname(entry.location.fsPath)), false); + }); + + test('removeAll', async () => { + let removed = false; + service.onDidRemoveEntries(() => removed = true); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy2.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy2.resource, source: 'My Source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + + await service.removeAll(CancellationToken.None); + + assert.strictEqual(removed, true); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + }); + + test('getEntries - simple', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry1); + + const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + assertEntryEqual(entries[1], entry2); + + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + const entry3 = await addEntry({ resource: workingCopy2.resource, source: 'other-test-source' }, CancellationToken.None); + + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry3); + }); + + test('getEntries - metadata preserved when stored', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy2.resource }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopy2.resource, source: 'other-source' }, CancellationToken.None); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry1); + + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + assertEntryEqual(entries[0], entry2); + assertEntryEqual(entries[1], entry3); + }); + + test('getEntries - corrupt meta.json is no problem', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry1 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + const metaFile = join(dirname(entry1.location.fsPath), 'entries.json'); + assert.ok(existsSync(metaFile)); + unlinkSync(metaFile); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry1, false /* skip timestamp that is unreliable when entries.json is gone */); + }); + + test('getEntries - missing entries from meta.json is no problem', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry1 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + unlinkSync(entry1.location.fsPath); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry2); + }); + + test('getEntries - in-memory and on-disk entries are merged', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry4 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + assertEntryEqual(entries[0], entry1); + assertEntryEqual(entries[1], entry2); + assertEntryEqual(entries[2], entry3); + assertEntryEqual(entries[3], entry4); + }); + + test('getEntries - configured max entries respected', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'Test source' }, CancellationToken.None); + const entry4 = await addEntry({ resource: workingCopy1.resource }, CancellationToken.None); + + service._configurationService.setUserConfiguration('workbench.localHistory.maxFileEntries', 2); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + assertEntryEqual(entries[0], entry3); + assertEntryEqual(entries[1], entry4); + + service._configurationService.setUserConfiguration('workbench.localHistory.maxFileEntries', 4); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + service._configurationService.setUserConfiguration('workbench.localHistory.maxFileEntries', 5); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + }); + + test('getAll', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + let resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 0); + + await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + await addEntry({ resource: workingCopy2.resource, source: 'test-source' }, CancellationToken.None); + await addEntry({ resource: workingCopy2.resource, source: 'test-source' }, CancellationToken.None); + + resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 2); + for (const resource of resources) { + if (resource.toString() !== workingCopy1.resource.toString() && resource.toString() !== workingCopy2.resource.toString()) { + assert.fail(`Unexpected history resource: ${resource.toString()}`); + } + } + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + const workingCopy3 = new TestWorkingCopy(URI.file(testFile3Path)); + await addEntry({ resource: workingCopy3.resource, source: 'test-source' }, CancellationToken.None); + + resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 3); + for (const resource of resources) { + if (resource.toString() !== workingCopy1.resource.toString() && resource.toString() !== workingCopy2.resource.toString() && resource.toString() !== workingCopy3.resource.toString()) { + assert.fail(`Unexpected history resource: ${resource.toString()}`); + } + } + }); + + test('getAll - ignores resource when no entries exist', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + + let resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 1); + + await service.removeEntry(entry, CancellationToken.None); + + resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 0); + + // Simulate shutdown + const event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + resources = await service.getAll(CancellationToken.None); + assert.strictEqual(resources.length, 0); + }); + + function assertEntryEqual(entryA: IWorkingCopyHistoryEntry, entryB: IWorkingCopyHistoryEntry, assertTimestamp = true): void { + assert.strictEqual(entryA.id, entryB.id); + assert.strictEqual(entryA.location.toString(), entryB.location.toString()); + if (assertTimestamp) { + assert.strictEqual(entryA.timestamp, entryB.timestamp); + } + assert.strictEqual(entryA.source, entryB.source); + assert.strictEqual(entryA.workingCopy.name, entryB.workingCopy.name); + assert.strictEqual(entryA.workingCopy.resource.toString(), entryB.workingCopy.resource.toString()); + } + + test('entries cleaned up on shutdown', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + const entry4 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + + service._configurationService.setUserConfiguration('workbench.localHistory.maxFileEntries', 2); + + // Simulate shutdown + let event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + assert.ok(!existsSync(entry1.location.fsPath)); + assert.ok(!existsSync(entry2.location.fsPath)); + assert.ok(existsSync(entry3.location.fsPath)); + assert.ok(existsSync(entry4.location.fsPath)); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 2); + assertEntryEqual(entries[0], entry3); + assertEntryEqual(entries[1], entry4); + + service._configurationService.setUserConfiguration('workbench.localHistory.maxFileEntries', 3); + + const entry5 = await addEntry({ resource: workingCopy1.resource, source: 'other-source' }, CancellationToken.None); + + // Simulate shutdown + event = new TestWillShutdownEvent(); + service._lifecycleService.fireWillShutdown(event); + await Promise.allSettled(event.value); + + assert.ok(existsSync(entry3.location.fsPath)); + assert.ok(existsSync(entry4.location.fsPath)); + assert.ok(existsSync(entry5.location.fsPath)); + + // Resolve from disk fresh and verify again + + service.dispose(); + service = new TestWorkingCopyHistoryService(testDir); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + assertEntryEqual(entries[0], entry3); + assertEntryEqual(entries[1], entry4); + assertEntryEqual(entries[2], entry5); + }); + + test('entries are merged when source is same', async () => { + let replaced: IWorkingCopyHistoryEntry | undefined = undefined; + service.onDidReplaceEntry(e => replaced = e.entry); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + + service._configurationService.setUserConfiguration('workbench.localHistory.mergePeriod', 1); + + const entry1 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + assert.strictEqual(replaced, undefined); + + const entry2 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + assert.strictEqual(replaced, entry1); + + const entry3 = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + assert.strictEqual(replaced, entry2); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 1); + assertEntryEqual(entries[0], entry3); + + service._configurationService.setUserConfiguration('workbench.localHistory.mergePeriod', undefined); + + await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + }); + + test('entries moved (file rename)', async () => { + const entriesMoved = Event.toPromise(service.onDidMoveEntries); + + const workingCopy = new TestWorkingCopy(URI.file(testFile1Path)); + + const entry1 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); + const entry2 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); + const entry3 = await addEntry({ resource: workingCopy.resource, source: 'test-source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + const renamedWorkingCopyResource = joinPath(resourcesDirname(workingCopy.resource), 'renamed.txt'); + await service._fileService.move(workingCopy.resource, renamedWorkingCopyResource); + + await entriesMoved; + + entries = await service.getEntries(workingCopy.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(renamedWorkingCopyResource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + assert.strictEqual(entries[0].id, entry1.id); + assert.strictEqual(entries[0].timestamp, entry1.timestamp); + assert.strictEqual(entries[0].source, entry1.source); + assert.notStrictEqual(entries[0].location, entry1.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); + + assert.strictEqual(entries[1].id, entry2.id); + assert.strictEqual(entries[1].timestamp, entry2.timestamp); + assert.strictEqual(entries[1].source, entry2.source); + assert.notStrictEqual(entries[1].location, entry2.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); + + assert.strictEqual(entries[2].id, entry3.id); + assert.strictEqual(entries[2].timestamp, entry3.timestamp); + assert.strictEqual(entries[2].source, entry3.source); + assert.notStrictEqual(entries[2].location, entry3.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopyResource.toString()); + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 1); + assert.strictEqual(all[0].toString(), renamedWorkingCopyResource.toString()); + }); + + test('entries moved (folder rename)', async () => { + const entriesMoved = Event.toPromise(service.onDidMoveEntries); + + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + const entry1A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry2A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + const entry3A = await addEntry({ resource: workingCopy1.resource, source: 'test-source' }, CancellationToken.None); + + const entry1B = await addEntry({ resource: workingCopy2.resource, source: 'test-source' }, CancellationToken.None); + const entry2B = await addEntry({ resource: workingCopy2.resource, source: 'test-source' }, CancellationToken.None); + const entry3B = await addEntry({ resource: workingCopy2.resource, source: 'test-source' }, CancellationToken.None); + + let entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 3); + + const renamedWorkHome = joinPath(resourcesDirname(URI.file(workHome)), 'renamed'); + await service._fileService.move(URI.file(workHome), renamedWorkHome); + + const renamedWorkingCopy1Resource = joinPath(renamedWorkHome, basename(workingCopy1.resource)); + const renamedWorkingCopy2Resource = joinPath(renamedWorkHome, basename(workingCopy2.resource)); + + await entriesMoved; + + entries = await service.getEntries(workingCopy1.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + entries = await service.getEntries(workingCopy2.resource, CancellationToken.None); + assert.strictEqual(entries.length, 0); + + entries = await service.getEntries(renamedWorkingCopy1Resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + assert.strictEqual(entries[0].id, entry1A.id); + assert.strictEqual(entries[0].timestamp, entry1A.timestamp); + assert.strictEqual(entries[0].source, entry1A.source); + assert.notStrictEqual(entries[0].location, entry1A.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), renamedWorkingCopy1Resource.toString()); + + assert.strictEqual(entries[1].id, entry2A.id); + assert.strictEqual(entries[1].timestamp, entry2A.timestamp); + assert.strictEqual(entries[1].source, entry2A.source); + assert.notStrictEqual(entries[1].location, entry2A.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), renamedWorkingCopy1Resource.toString()); + + assert.strictEqual(entries[2].id, entry3A.id); + assert.strictEqual(entries[2].timestamp, entry3A.timestamp); + assert.strictEqual(entries[2].source, entry3A.source); + assert.notStrictEqual(entries[2].location, entry3A.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopy1Resource.toString()); + + entries = await service.getEntries(renamedWorkingCopy2Resource, CancellationToken.None); + assert.strictEqual(entries.length, 4); + + assert.strictEqual(entries[0].id, entry1B.id); + assert.strictEqual(entries[0].timestamp, entry1B.timestamp); + assert.strictEqual(entries[0].source, entry1B.source); + assert.notStrictEqual(entries[0].location, entry1B.location); + assert.strictEqual(entries[0].workingCopy.resource.toString(), renamedWorkingCopy2Resource.toString()); + + assert.strictEqual(entries[1].id, entry2B.id); + assert.strictEqual(entries[1].timestamp, entry2B.timestamp); + assert.strictEqual(entries[1].source, entry2B.source); + assert.notStrictEqual(entries[1].location, entry2B.location); + assert.strictEqual(entries[1].workingCopy.resource.toString(), renamedWorkingCopy2Resource.toString()); + + assert.strictEqual(entries[2].id, entry3B.id); + assert.strictEqual(entries[2].timestamp, entry3B.timestamp); + assert.strictEqual(entries[2].source, entry3B.source); + assert.notStrictEqual(entries[2].location, entry3B.location); + assert.strictEqual(entries[2].workingCopy.resource.toString(), renamedWorkingCopy2Resource.toString()); + + const all = await service.getAll(CancellationToken.None); + assert.strictEqual(all.length, 2); + for (const resource of all) { + if (resource.toString() !== renamedWorkingCopy1Resource.toString() && resource.toString() !== renamedWorkingCopy2Resource.toString()) { + assert.fail(`Unexpected history resource: ${resource.toString()}`); + } + } + }); +}); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryTracker.test.ts new file mode 100644 index 00000000000..b02ac560a03 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryTracker.test.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { flakySuite } from 'vs/base/test/common/testUtils'; +import { TestContextService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { Promises } from 'vs/base/node/pfs'; +import { URI } from 'vs/base/common/uri'; +import { TestWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test'; +import { WorkingCopyHistoryTracker } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryTracker'; +import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; +import { TestFileService, TestPathService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { DeferredPromise } from 'vs/base/common/async'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; + +flakySuite('WorkingCopyHistoryTracker', () => { + + let testDir: string; + let historyHome: string; + + let workingCopyHistoryService: TestWorkingCopyHistoryService; + let workingCopyService: WorkingCopyService; + let fileService: IFileService; + let configurationService: TestConfigurationService; + + let tracker: WorkingCopyHistoryTracker; + + let testFile1Path: string; + let testFile2Path: string; + + const testFile1PathContents = 'Hello Foo'; + const testFile2PathContents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join('').repeat(1000); + + setup(async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'workingcopyhistorytracker'); + historyHome = join(testDir, 'User', 'History'); + + workingCopyHistoryService = new TestWorkingCopyHistoryService(testDir); + workingCopyService = new WorkingCopyService(); + fileService = workingCopyHistoryService._fileService; + configurationService = workingCopyHistoryService._configurationService; + + tracker = createTracker(); + + await Promises.mkdir(historyHome, { recursive: true }); + + testFile1Path = join(testDir, 'foo.txt'); + testFile2Path = join(testDir, 'bar.txt'); + + await Promises.writeFile(testFile1Path, testFile1PathContents); + await Promises.writeFile(testFile2Path, testFile2PathContents); + }); + + function createTracker() { + return new WorkingCopyHistoryTracker( + workingCopyService, + workingCopyHistoryService, + new UriIdentityService(new TestFileService()), + new TestPathService(undefined, Schemas.file), + configurationService, + new UndoRedoService(new TestDialogService(), new TestNotificationService()), + new TestContextService() + ); + } + + teardown(() => { + workingCopyHistoryService.dispose(); + workingCopyService.dispose(); + tracker.dispose(); + + return Promises.rm(testDir); + }); + + test('history entry added on save', async () => { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + const stat1 = await fileService.resolve(workingCopy1.resource, { resolveMetadata: true }); + const stat2 = await fileService.resolve(workingCopy2.resource, { resolveMetadata: true }); + + workingCopyService.registerWorkingCopy(workingCopy1); + workingCopyService.registerWorkingCopy(workingCopy2); + + const saveResult = new DeferredPromise(); + let addedCounter = 0; + workingCopyHistoryService.onDidAddEntry(e => { + if (isEqual(e.entry.workingCopy.resource, workingCopy1.resource) || isEqual(e.entry.workingCopy.resource, workingCopy2.resource)) { + addedCounter++; + + if (addedCounter === 2) { + saveResult.complete(); + } + } + }); + + await workingCopy1.save(undefined, stat1); + await workingCopy2.save(undefined, stat2); + + await saveResult.p; + }); + + test('history entry skipped when setting disabled (globally)', async () => { + configurationService.setUserConfiguration('workbench.localHistory.enabled', false, URI.file(testFile1Path)); + + return assertNoLocalHistoryEntryAddedWithSettingsConfigured(); + }); + + test('history entry skipped when setting disabled (exclude)', () => { + configurationService.setUserConfiguration('workbench.localHistory.exclude', { '**/foo.txt': true }); + + // Recreate to apply settings + tracker.dispose(); + tracker = createTracker(); + + return assertNoLocalHistoryEntryAddedWithSettingsConfigured(); + }); + + test('history entry skipped when too large', async () => { + configurationService.setUserConfiguration('workbench.localHistory.maxFileSize', 0, URI.file(testFile1Path)); + + return assertNoLocalHistoryEntryAddedWithSettingsConfigured(); + }); + + async function assertNoLocalHistoryEntryAddedWithSettingsConfigured(): Promise { + const workingCopy1 = new TestWorkingCopy(URI.file(testFile1Path)); + const workingCopy2 = new TestWorkingCopy(URI.file(testFile2Path)); + + const stat1 = await fileService.resolve(workingCopy1.resource, { resolveMetadata: true }); + const stat2 = await fileService.resolve(workingCopy2.resource, { resolveMetadata: true }); + + workingCopyService.registerWorkingCopy(workingCopy1); + workingCopyService.registerWorkingCopy(workingCopy2); + + const saveResult = new DeferredPromise(); + workingCopyHistoryService.onDidAddEntry(e => { + if (isEqual(e.entry.workingCopy.resource, workingCopy1.resource)) { + assert.fail('Unexpected working copy history entry: ' + e.entry.workingCopy.resource.toString()); + } + + if (isEqual(e.entry.workingCopy.resource, workingCopy2.resource)) { + saveResult.complete(); + } + }); + + await workingCopy1.save(undefined, stat1); + await workingCopy2.save(undefined, stat2); + + await saveResult.p; + } +}); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index dd701e14629..c8c83c777c5 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -22,7 +22,7 @@ import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogSer import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; @@ -34,6 +34,7 @@ import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/te import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IStorageService } from 'vs/platform/storage/common/storage'; suite('EditorModel', () => { @@ -61,6 +62,7 @@ suite('EditorModel', () => { instantiationService.stub(IEditorService, new TestEditorService()); instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(ILanguageConfigurationService, new TestLanguageConfigurationService()); + instantiationService.stub(IStorageService, new TestStorageService()); return instantiationService.createInstance(ModelService); } diff --git a/src/vs/workbench/test/browser/webview.test.ts b/src/vs/workbench/test/browser/webview.test.ts new file mode 100644 index 00000000000..a483b086d8b --- /dev/null +++ b/src/vs/workbench/test/browser/webview.test.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { parentOriginHash } from 'vs/workbench/browser/webview'; + +suite('parentOriginHash', () => { + + test('localhost 1', async () => { + const hash = await parentOriginHash('http://localhost:9888', '123456'); + assert.strictEqual(hash, '0fnsiac2jaup1t266qekgr7iuj4pnm31gf8r0h1o6k2lvvmfh6hk'); + }); + + test('localhost 2', async () => { + const hash = await parentOriginHash('http://localhost:9888', '123457'); + assert.strictEqual(hash, '07shf01bmdfrghk96voldpletbh36vj7blnl4td8kdq1sej5kjqs'); + }); + + test('localhost 3', async () => { + const hash = await parentOriginHash('http://localhost:9887', '123456'); + assert.strictEqual(hash, '1v1128i162q0nee9l89360sqan26u3pdnjrkke5ijd0sel8sbtqf'); + }); +}); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index a31b2f606a4..edb770bfe7b 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -43,7 +43,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model'; -import { IRange, Range } from 'vs/editor/common/core/range'; +import { Range } from 'vs/editor/common/core/range'; import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -98,7 +98,7 @@ import { IInputBox, IInputOptions, IPickOptions, IQuickInputButton, IQuickInputS import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; import { IListService } from 'vs/platform/list/browser/listService'; import { win32, posix } from 'vs/base/common/path'; -import { TestContextService, TestStorageService, TestTextResourcePropertiesService, TestExtensionService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService, TestTextResourcePropertiesService, TestExtensionService, TestProductService, createFileStat } from 'vs/workbench/test/common/workbenchTestServices'; import { IViewsService, IView, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -132,8 +132,7 @@ import { IEditorResolverService } from 'vs/workbench/services/editor/common/edit import { IWorkingCopyEditorService, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService'; -import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorker'; -import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/languages'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { ResourceMap } from 'vs/base/common/map'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; import { ITextEditorService, TextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; @@ -148,12 +147,12 @@ import { env } from 'vs/base/common/process'; import { isValidBasename } from 'vs/base/common/extpath'; import { TestAccessibilityService } from 'vs/platform/accessibility/test/common/testAccessibilityService'; import { ILanguageFeatureDebounceService, LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; -import { IChange, IDiffComputationResult } from 'vs/editor/common/diff/diffComputer'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; import { TextEditorPaneSelection } from 'vs/workbench/browser/parts/editor/textEditor'; import { Selection } from 'vs/editor/common/core/selection'; import { IFolderBackupInfo, IWorkspaceBackupInfo } from 'vs/platform/backup/common/backup'; +import { TestEditorWorkerService } from 'vs/editor/test/common/services/testEditorWorkerService'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined, undefined); @@ -930,8 +929,10 @@ export class TestEditorService implements EditorServiceImpl { openEditor(editor: IResourceEditorInput | IUntitledTextResourceEditorInput, group?: PreferredGroup): Promise; openEditor(editor: IResourceDiffEditorInput, group?: PreferredGroup): Promise; async openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrGroup?: IEditorOptions | PreferredGroup, group?: PreferredGroup): Promise { - throw new Error('not implemented'); + return undefined; } + async closeEditor(editor: IEditorIdentifier, options?: ICloseEditorOptions): Promise { } + async closeEditors(editors: IEditorIdentifier[], options?: ICloseEditorOptions): Promise { } doResolveEditorOpenRequest(editor: EditorInput | IUntypedEditorInput): [IEditorGroup, EditorInput, IEditorOptions | undefined] | undefined { if (!this.editorGroupService) { return undefined; @@ -979,20 +980,8 @@ export class TestFileService implements IFileService { resolve(resource: URI, _options: IResolveMetadataFileOptions): Promise; resolve(resource: URI, _options?: IResolveFileOptions): Promise; - resolve(resource: URI, _options?: IResolveFileOptions): Promise { - return Promise.resolve({ - resource, - etag: Date.now().toString(), - encoding: 'utf8', - mtime: Date.now(), - size: 42, - isFile: true, - isDirectory: false, - isSymbolicLink: false, - readonly: this.readonly, - name: basename(resource), - children: undefined - }); + async resolve(resource: URI, _options?: IResolveFileOptions): Promise { + return createFileStat(resource, this.readonly); } stat(resource: URI): Promise { @@ -1011,44 +1000,30 @@ export class TestFileService implements IFileService { readShouldThrowError: Error | undefined = undefined; - readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { + async readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { if (this.readShouldThrowError) { throw this.readShouldThrowError; } this.lastReadFileUri = resource; - return Promise.resolve({ - resource: resource, - value: VSBuffer.fromString(this.content), - etag: 'index.txt', - encoding: 'utf8', - mtime: Date.now(), - ctime: Date.now(), - name: basename(resource), - readonly: this.readonly, - size: 1 - }); + return { + ...createFileStat(resource, this.readonly), + value: VSBuffer.fromString(this.content) + }; } - readFileStream(resource: URI, options?: IReadFileStreamOptions | undefined): Promise { + async readFileStream(resource: URI, options?: IReadFileStreamOptions | undefined): Promise { if (this.readShouldThrowError) { throw this.readShouldThrowError; } this.lastReadFileUri = resource; - return Promise.resolve({ - resource, - value: bufferToStream(VSBuffer.fromString(this.content)), - etag: 'index.txt', - encoding: 'utf8', - mtime: Date.now(), - ctime: Date.now(), - size: 1, - readonly: this.readonly, - name: basename(resource) - }); + return { + ...createFileStat(resource, this.readonly), + value: bufferToStream(VSBuffer.fromString(this.content)) + }; } writeShouldThrowError: Error | undefined = undefined; @@ -1060,23 +1035,12 @@ export class TestFileService implements IFileService { throw this.writeShouldThrowError; } - return ({ - resource, - etag: 'index.txt', - mtime: Date.now(), - ctime: Date.now(), - size: 42, - isFile: true, - isDirectory: false, - isSymbolicLink: false, - readonly: this.readonly, - name: basename(resource), - children: undefined - }); + return createFileStat(resource, this.readonly); } move(_source: URI, _target: URI, _overwrite?: boolean): Promise { return Promise.resolve(null!); } copy(_source: URI, _target: URI, _overwrite?: boolean): Promise { return Promise.resolve(null!); } + async cloneFile(_source: URI, _target: URI): Promise { } createFile(_resource: URI, _content?: VSBuffer | VSBufferReadable, _options?: ICreateFileOptions): Promise { return Promise.resolve(null!); } createFolder(_resource: URI): Promise { return Promise.resolve(null!); } @@ -1176,7 +1140,7 @@ export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBack const logService = new NullLogService(); const fileService = new FileService(logService); fileService.registerProvider(Schemas.file, new InMemoryFileSystemProvider()); - fileService.registerProvider(Schemas.userData, new InMemoryFileSystemProvider()); + fileService.registerProvider(Schemas.vscodeUserData, new InMemoryFileSystemProvider()); super(new TestContextService(TestWorkspace), environmentService, fileService, logService); @@ -1254,6 +1218,7 @@ export class TestLifecycleService implements ILifecycleService { this.shutdownJoiners.push(p); }, force: () => { /* No-Op in tests */ }, + token: CancellationToken.None, reason }); } @@ -1287,6 +1252,7 @@ export class TestWillShutdownEvent implements WillShutdownEvent { value: Promise[] = []; reason = ShutdownReason.CLOSE; + token = CancellationToken.None; join(promise: Promise, id: string): void { this.value.push(promise); @@ -1699,7 +1665,7 @@ export class TestPathService implements IPathService { declare readonly _serviceBrand: undefined; - constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.vscodeRemote, path: '/' })) { } + constructor(private readonly fallbackUserHome: URI = URI.from({ scheme: Schemas.vscodeRemote, path: '/' }), public defaultUriScheme = Schemas.vscodeRemote) { } hasValidBasename(resource: URI, basename?: string): Promise; hasValidBasename(resource: URI, os: OperatingSystem, basename?: string): boolean; @@ -1719,8 +1685,6 @@ export class TestPathService implements IPathService { async fileURI(path: string): Promise { return URI.file(path); } - - readonly defaultUriScheme = Schemas.vscodeRemote; } export class TestTextFileEditorModelManager extends TextFileEditorModelManager { @@ -1900,19 +1864,3 @@ export class TestQuickInputService implements IQuickInputService { back(): Promise { throw new Error('not implemented.'); } cancel(): Promise { throw new Error('not implemented.'); } } - -export class TestEditorWorkerService implements IEditorWorkerService { - - declare readonly _serviceBrand: undefined; - - canComputeUnicodeHighlights(uri: URI): boolean { return false; } - async computedUnicodeHighlights(uri: URI): Promise { return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; } - async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { return null; } - canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; } - async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return null; } - async computeMoreMinimalEdits(resource: URI, edits: TextEdit[] | null | undefined): Promise { return undefined; } - canComputeWordRanges(resource: URI): boolean { return false; } - async computeWordRanges(resource: URI, range: IRange): Promise<{ [word: string]: IRange[] } | null> { return null; } - canNavigateValueSet(resource: URI): boolean { return false; } - async navigateValueSet(resource: URI, range: IRange, up: boolean): Promise { return null; } -} diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index fcd5ac00b2c..dfe49276cef 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -22,6 +22,7 @@ import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/ed import { CancellationToken } from 'vs/base/common/cancellation'; import product from 'vs/platform/product/common/product'; import { IActivity, IActivityService } from 'vs/workbench/services/activity/common/activity'; +import { IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { @@ -139,6 +140,9 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; + private readonly _onDidSave = this._register(new Emitter()); + readonly onDidSave = this._onDidSave.event; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.resource); @@ -166,7 +170,9 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { return this.dirty; } - async save(options?: ISaveOptions): Promise { + async save(options?: ISaveOptions, stat?: IFileStatWithMetadata): Promise { + this._onDidSave.fire({ reason: options?.reason ?? SaveReason.EXPLICIT, stat: stat ?? createFileStat(this.resource), source: options?.source }); + return true; } @@ -179,6 +185,22 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { } } +export function createFileStat(resource: URI, readonly = false): IFileStatWithMetadata { + return { + resource, + etag: Date.now().toString(), + mtime: Date.now(), + ctime: Date.now(), + size: 42, + isFile: true, + isDirectory: false, + isSymbolicLink: false, + readonly, + name: basename(resource), + children: undefined + }; +} + export class TestWorkingCopyFileService implements IWorkingCopyFileService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 6880a01741a..dfc2bcadef0 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -96,8 +96,8 @@ import 'vs/workbench/services/hover/browser/hoverService'; import 'vs/workbench/services/assignment/common/assignmentService'; import 'vs/workbench/services/outline/browser/outlineService'; import 'vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl'; - import 'vs/editor/common/services/languageFeaturesService'; + import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; @@ -288,7 +288,6 @@ import 'vs/workbench/contrib/surveys/browser/languageSurveys.contribution'; // Welcome import 'vs/workbench/contrib/welcomeOverlay/browser/welcomeOverlay'; -import 'vs/workbench/contrib/welcomePage/browser/welcomePage.contribution'; import 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution'; import 'vs/workbench/contrib/welcomeWalkthrough/browser/walkThrough.contribution'; import 'vs/workbench/contrib/welcomeViews/common/viewsWelcome.contribution'; @@ -322,6 +321,9 @@ import 'vs/workbench/contrib/codeActions/browser/codeActions.contribution'; // Timeline import 'vs/workbench/contrib/timeline/browser/timeline.contribution'; +// Local History +import 'vs/workbench/contrib/localHistory/browser/localHistory.contribution'; + // Workspace import 'vs/workbench/contrib/workspace/browser/workspace.contribution'; diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 4b363e7b336..2150dd3f14b 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -80,6 +80,7 @@ import 'vs/platform/profiling/electron-sandbox/profilingService'; import 'vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService'; import 'vs/workbench/services/files/electron-sandbox/elevatedFileService'; import 'vs/workbench/services/search/electron-sandbox/searchService'; +import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; @@ -153,4 +154,7 @@ import 'vs/workbench/contrib/webview/electron-sandbox/webview.contribution'; // Splash import 'vs/workbench/contrib/splash/electron-sandbox/splash.contribution'; +// Local History +import 'vs/workbench/contrib/localHistory/electron-sandbox/localHistory.contribution'; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 757d9d91f7a..cb8ee5c4ce7 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -59,6 +59,7 @@ import 'vs/workbench/services/encryption/browser/encryptionService'; import 'vs/workbench/services/workingCopy/browser/workingCopyBackupService'; import 'vs/workbench/services/tunnel/browser/tunnelService'; import 'vs/workbench/services/files/browser/elevatedFileService'; +import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index e0fceca624d..a76554f52a9 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -2064,6 +2064,18 @@ declare module 'vscode' { */ readonly language?: string; + /** + * The {@link NotebookDocument.notebookType type} of a notebook, like `jupyter-notebook`. This allows + * to narrow down on the type of a notebook that a {@link NotebookCell.document cell document} belongs to. + * + * *Note* that setting the `notebookType`-property changes how `scheme` and `pattern` are interpreted. When set + * they are evaluated against the {@link NotebookDocument.uri notebook uri}, not the document uri. + * + * @example Match python document inside jupyter notebook that aren't stored yet (`untitled`) + * { language: 'python', notebookType: 'jupyter-notebook', scheme: 'untitled' } + */ + readonly notebookType?: string; + /** * A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. */ @@ -2660,6 +2672,27 @@ declare module 'vscode' { */ supportHtml?: boolean; + /** + * Uri that relative paths are resolved relative to. + * + * If the `baseUri` ends with `/`, it is considered a directory and relative paths in the markdown are resolved relative to that directory: + * + * ```ts + * const md = new vscode.MarkdownString(`[link](./file.js)`); + * md.baseUri = vscode.Uri.file('/path/to/dir/'); + * // Here 'link' in the rendered markdown resolves to '/path/to/dir/file.js' + * ``` + * + * If the `baseUri` is a file, relative paths in the markdown are resolved relative to the parent dir of that file: + * + * ```ts + * const md = new vscode.MarkdownString(`[link](./file.js)`); + * md.baseUri = vscode.Uri.file('/path/to/otherFile.js'); + * // Here 'link' in the rendered markdown resolves to '/path/to/file.js' + * ``` + */ + baseUri?: Uri; + /** * Creates a new markdown string with the given value. * @@ -4496,7 +4529,6 @@ declare module 'vscode' { * * @param range The range the color appears in. Must not be empty. * @param color The value of the color. - * @param format The format in which this color is currently formatted. */ constructor(range: Range, color: Color); } @@ -4654,6 +4686,9 @@ declare module 'vscode' { /** * The tooltip text when you hover over this item. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ tooltip?: string | MarkdownString | undefined; @@ -4662,6 +4697,18 @@ declare module 'vscode' { */ kind?: InlayHintKind; + /** + * Optional {@link TextEdit text edits} that are performed when accepting this inlay hint. The default + * gesture for accepting an inlay hint is the double click. + * + * *Note* that edits are expected to change the document so that the inlay hint (or its nearest variant) is + * now part of the document and the inlay hint itself is now obsolete. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + textEdits?: TextEdit[]; + /** * Render padding before the hint. Padding will use the editor's background color, * not the background color of the hint itself. That means padding can be used to visually @@ -4710,8 +4757,8 @@ declare module 'vscode' { provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; /** - * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.command command}, or complete - * label {@link InlayHintLabelPart parts}. + * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.textEdits text edits}, + * or complete label {@link InlayHintLabelPart parts}. * * *Note* that the editor will resolve an inlay hint at most once. * @@ -6491,8 +6538,8 @@ declare module 'vscode' { extensionKind: ExtensionKind; /** - * The public API exported by this extension. It is an invalid action - * to access this field before this extension has been activated. + * The public API exported by this extension (return value of `activate`). + * It is an invalid action to access this field before this extension has been activated. */ readonly exports: T; @@ -6778,7 +6825,8 @@ declare module 'vscode' { export enum ColorThemeKind { Light = 1, Dark = 2, - HighContrast = 3 + HighContrast = 3, + HighContrastLight = 4 } /** @@ -6787,7 +6835,7 @@ declare module 'vscode' { export interface ColorTheme { /** - * The kind of this color theme: light, dark or high contrast. + * The kind of this color theme: light, dark, high contrast dark and high contrast light. */ readonly kind: ColorThemeKind; } @@ -7206,7 +7254,7 @@ declare module 'vscode' { /** * Creates a new task. * - * @param definition The task definition as defined in the taskDefinitions extension point. + * @param taskDefinition The task definition as defined in the taskDefinitions extension point. * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. Global tasks are currently not supported. * @param name The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. @@ -7222,7 +7270,7 @@ declare module 'vscode' { * * @deprecated Use the new constructors that allow specifying a scope for the task. * - * @param definition The task definition as defined in the taskDefinitions extension point. + * @param taskDefinition The task definition as defined in the taskDefinitions extension point. * @param name The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. * @param execution The process or shell execution. @@ -7859,8 +7907,8 @@ declare module 'vscode' { /** * Rename a file or folder. * - * @param oldUri The existing file. - * @param newUri The new location. + * @param source The existing file. + * @param target The new location. * @param options Defines if existing files should be overwritten. */ rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; @@ -7869,7 +7917,7 @@ declare module 'vscode' { * Copy files or folders. * * @param source The existing file. - * @param destination The destination location. + * @param target The destination location. * @param options Defines if existing files should be overwritten. */ copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; @@ -9355,11 +9403,16 @@ declare module 'vscode' { export function createInputBox(): InputBox; /** - * Creates a new {@link OutputChannel output channel} with the given name. + * Creates a new {@link OutputChannel output channel} with the given name and language id + * If language id is not provided, then **Log** is used as default language id. + * + * You can access the visible or active output channel as a {@link TextDocument text document} from {@link window.visibleTextEditors visible editors} or {@link window.activeTextEditor active editor} + * and use the langage id to contribute language features like syntax coloring, code lens etc., * * @param name Human-readable string which will be used to represent the channel in the UI. + * @param languageId The identifier of the language associated with the channel. */ - export function createOutputChannel(name: string): OutputChannel; + export function createOutputChannel(name: string, languageId?: string): OutputChannel; /** * Create and show a new webview panel. @@ -9669,6 +9722,11 @@ declare module 'vscode' { * array containing all selected tree items. */ canSelectMany?: boolean; + + /** + * An optional interface to implement drag and drop in the tree view. + */ + dragAndDropController?: TreeDragAndDropController; } /** @@ -9707,6 +9765,99 @@ declare module 'vscode' { } + /** + * A class for encapsulating data transferred during a drag and drop event. + * + * You can use the `value` of the `DataTransferItem` to get back the object you put into it + * so long as the extension that created the `DataTransferItem` runs in the same extension host. + */ + export class DataTransferItem { + asString(): Thenable; + readonly value: any; + constructor(value: any); + } + + /** + * A map containing a mapping of the mime type of the corresponding transferred data. + * Drag and drop controllers that implement `handleDrag` can additional mime types to the data transfer + * These additional mime types will only be included in the `handleDrop` when the the drag was initiated from + * an element in the same drag and drop controller. + */ + export class DataTransfer { + /** + * Retrieves the data transfer item for a given mime type. + * @param mimeType The mime type to get the data transfer item for. + */ + get(mimeType: string): T | undefined; + + /** + * Sets a mime type to data transfer item mapping. + * @param mimeType The mime type to set the data for. + * @param value The data transfer item for the given mime type. + */ + set(mimeType: string, value: T): void; + + /** + * Allows iteration through the data transfer items. + * @param callbackfn Callback for iteration through the data transfer items. + */ + forEach(callbackfn: (value: T, key: string) => void): void; + } + + /** + * Provides support for drag and drop in `TreeView`. + */ + export interface TreeDragAndDropController { + + /** + * The mime types that the `handleDrop` method of this `DragAndDropController` supports. + * This could be well-defined, existing, mime types, and also mime types defined by the extension. + * + * Each tree will automatically support drops from it's own `DragAndDropController`. To support drops from other trees, + * you will need to add the mime type of that tree. The mime type of a tree is of the format `application/vnd.code.tree.treeidlowercase`. + * + * To learn the mime type of a dragged item: + * 1. Set up your `DragAndDropController` + * 2. Use the Developer: Set Log Level... command to set the level to "Debug" + * 3. Open the developer tools and drag the item with unknown mime type over your tree. The mime types will be logged to the developer console + */ + readonly dropMimeTypes: string[]; + + /** + * The mime types that the `handleDrag` method of this `TreeDragAndDropController` may add to the tree data transfer. + * This could be well-defined, existing, mime types, and also mime types defined by the extension. + */ + readonly dragMimeTypes: string[]; + + /** + * When the user starts dragging items from this `DragAndDropController`, `handleDrag` will be called. + * Extensions can use `handleDrag` to add their `DataTransferItem`s to the drag and drop. + * + * When the items are dropped on **another tree item** in **the same tree**, your `DataTransferItem` objects + * will be preserved. See the documentation for `DataTransferItem` for how best to take advantage of this. + * + * To add a data transfer item that can be dragged into the editor, use the application specific mime type "text/uri-list". + * The data for "text/uri-list" should be a string with `toString()`ed Uris separated by newlines. To specify a cursor position in the file, + * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number. + * + * @param source The source items for the drag and drop operation. + * @param treeDataTransfer The data transfer associated with this drag. + * @param token A cancellation token indicating that drag has been cancelled. + */ + handleDrag?(source: T[], treeDataTransfer: DataTransfer, token: CancellationToken): Thenable | void; + + /** + * Called when a drag and drop action results in a drop on the tree that this `DragAndDropController` belongs too. + * + * Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed. + * + * @param source The data transfer items of the source of the drag. + * @param target The target tree element that the drop is occurring on. + * @param token A cancellation token indicating that the drop has been cancelled. + */ + handleDrop?(target: T, source: DataTransfer, token: CancellationToken): Thenable | void; + } + /** * Represents a Tree view */ @@ -9784,7 +9935,7 @@ declare module 'vscode' { * This will trigger the view to update the changed element/root and its children recursively (if shown). * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. */ - onDidChangeTreeData?: Event; + onDidChangeTreeData?: Event; /** * Get {@link TreeItem} representation of the `element` @@ -11765,10 +11916,10 @@ declare module 'vscode' { * 1. When {@linkcode DocumentSelector} is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. * 2. A string will be desugared to become the `language`-part of a {@linkcode DocumentFilter}, so `"fooLang"` is like `{ language: "fooLang" }`. * 3. A {@linkcode DocumentFilter} will be matched against the document by comparing its parts with the document. The following rules apply: - * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` - * 2. When `scheme`, `language`, or `pattern` are defined but one doesn't match, the result is `0` - * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` - * 4. The result is the maximum value of each match + * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` + * 2. When `scheme`, `language`, `pattern`, or `notebook` are defined but one doesn't match, the result is `0` + * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` + * 4. The result is the maximum value of each match * * Samples: * ```js @@ -11776,8 +11927,8 @@ declare module 'vscode' { * doc.uri; //'file:///my/file.js' * doc.languageId; // 'javascript' * match('javascript', doc); // 10; - * match({language: 'javascript'}, doc); // 10; - * match({language: 'javascript', scheme: 'file'}, doc); // 10; + * match({ language: 'javascript' }, doc); // 10; + * match({ language: 'javascript', scheme: 'file' }, doc); // 10; * match('*', doc); // 5 * match('fooLang', doc); // 0 * match(['fooLang', '*'], doc); // 5 @@ -11786,8 +11937,16 @@ declare module 'vscode' { * doc.uri; // 'git:/my/file.js' * doc.languageId; // 'javascript' * match('javascript', doc); // 10; - * match({language: 'javascript', scheme: 'git'}, doc); // 10; + * match({ language: 'javascript', scheme: 'git' }, doc); // 10; * match('*', doc); // 5 + * + * // notebook cell document + * doc.uri; // `vscode-notebook-cell:///my/notebook.ipynb#gl65s2pmha`; + * doc.languageId; // 'python' + * match({ notebookType: 'jupyter-notebook' }, doc) // 10 + * match({ notebookType: 'fooNotebook', language: 'python' }, doc) // 0 + * match({ language: 'python' }, doc) // 10 + * match({ notebookType: '*' }, doc) // 5 * ``` * * @param selector A document selector. @@ -13924,7 +14083,7 @@ declare module 'vscode' { * Registering a single provider with resolve methods for different trigger kinds, results in the same resolve methods called multiple times. * More than one provider can be registered for the same type. * - * @param type The debug type for which the provider is registered. + * @param debugType The debug type for which the provider is registered. * @param provider The {@link DebugConfigurationProvider debug configuration provider} to register. * @param triggerKind The {@link DebugConfigurationProviderTrigger trigger} for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. * @return A {@link Disposable} that unregisters this provider when being disposed. @@ -14883,7 +15042,7 @@ declare module 'vscode' { readonly profile: TestRunProfile | undefined; /** - * @param tests Array of specific tests to run, or undefined to run all tests + * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. */ @@ -14934,7 +15093,7 @@ declare module 'vscode' { * Indicates a test has failed. You should pass one or more * {@link TestMessage TestMessages} to describe the failure. * @param test Test item to update. - * @param messages Messages associated with the test failure. + * @param message Messages associated with the test failure. * @param duration How long the test took to execute, in milliseconds. */ failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; @@ -14945,7 +15104,7 @@ declare module 'vscode' { * from the "failed" state in that it indicates a test that couldn't be * executed at all, from a compilation error for example. * @param test Test item to update. - * @param messages Messages associated with the test failure. + * @param message Messages associated with the test failure. * @param duration How long the test took to execute, in milliseconds. */ errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; @@ -15003,7 +15162,7 @@ declare module 'vscode' { /** * Adds the test item to the children. If an item with the same ID already * exists, it'll be replaced. - * @param items Item to add. + * @param item Item to add. */ add(item: TestItem): void; diff --git a/src/vscode-dts/vscode.proposed.badges.d.ts b/src/vscode-dts/vscode.proposed.badges.d.ts new file mode 100644 index 00000000000..5aed72b6019 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.badges.d.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/62783 @matthewjamesadam + + /** + * A badge presenting a value for a view + */ + export interface ViewBadge { + + /** + * A label to present in tooltips for the badge + */ + readonly tooltip: string; + + /** + * The value to present in the badge + */ + readonly value: number; + } + + export interface TreeView { + /** + * The badge to display for this TreeView. + * To remove the badge, set to undefined. + */ + badge?: ViewBadge | undefined; + } + + export interface WebviewView { + /** + * The badge to display for this webview view. + * To remove the badge, set to undefined. + */ + badge?: ViewBadge | undefined; + } +} diff --git a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts index 563cab4c4de..bf01c4dca4b 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts @@ -48,10 +48,13 @@ declare module 'vscode' { readonly selectedCompletionInfo: SelectedCompletionInfo | undefined; } - // TODO@API strongly consider to use vscode.TextEdit instead + // TODO@API remove kind, snippet properties + // TODO@API find a better name, xyzFilter, xyzConstraint export interface SelectedCompletionInfo { range: Range; text: string; + + completionKind: CompletionItemKind; isSnippetText: boolean; } @@ -81,9 +84,14 @@ declare module 'vscode' { */ // TODO@API We could keep this and allow for `vscode.Command` instances that explain // the result. That would replace the existing proposed menu-identifier and be more LSP friendly + // TODO@API maybe use MarkdownString export class InlineCompletionList { items: T[]; + // command: Command; "Show More..." + + // description: MarkdownString + /** * @deprecated Return an array of Inline Completion items directly. Will be removed eventually. */ @@ -101,8 +109,7 @@ declare module 'vscode' { * However, any indentation of the text to replace does not matter for the subword constraint. * Thus, ` B` can be replaced with ` ABC`, effectively removing a whitespace and inserting `A` and `C`. */ - // TODO@API support vscode.SnippetString in addition to string, see CompletionItem#insertText - insertText?: string; + insertText?: string | SnippetString; /** * @deprecated Use `insertText` instead. Will be removed eventually. @@ -129,6 +136,7 @@ declare module 'vscode' { } + // TODO@API move "never" API into new proposal export interface InlineCompletionItem { /** @@ -142,6 +150,7 @@ declare module 'vscode' { * Be aware that this API will not ever be finalized. */ export namespace window { + // TODO@API move into provider (just like internal API). Only read property if proposal is enabled! export function getInlineCompletionItemController(provider: InlineCompletionItemProvider): InlineCompletionController; } diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts new file mode 100644 index 00000000000..c79cf048df9 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * 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/124024 @hediet @alexdima + + export interface InlineCompletionItemNew { + /** + * If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed. + * Defaults to `false`. + */ + completeBracketPairs?: boolean; + } + + export interface InlineCompletionItemProviderNew { + // eslint-disable-next-line vscode-dts-provider-naming + handleDidShowCompletionItem?(completionItem: InlineCompletionItemNew): void; + } +} diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts new file mode 100644 index 00000000000..7855c4b4ea4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * 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/124024 @hediet @alexdima + // Temporary API to allow for safe migration. + + export namespace languages { + /** + * Registers an inline completion provider. + * + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + // TODO@API what are the rules when multiple providers apply + export function registerInlineCompletionItemProviderNew(selector: DocumentSelector, provider: InlineCompletionItemProviderNew): Disposable; + } + + export interface InlineCompletionItemProviderNew { + /** + * Provides inline completion items for the given position and document. + * If inline completions are enabled, this method will be called whenever the user stopped typing. + * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. + * Use `context.triggerKind` to distinguish between these scenarios. + */ + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContextNew, token: CancellationToken): ProviderResult; + } + + export interface InlineCompletionContextNew { + /** + * How the completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKindNew; + + /** + * Provides information about the currently selected item in the autocomplete widget if it is visible. + * + * If set, provided inline completions must extend the text of the selected item + * and use the same range, otherwise they are not shown as preview. + * As an example, if the document text is `console.` and the selected item is `.log` replacing the `.` in the document, + * the inline completion must also replace `.` and start with `.log`, for example `.log()`. + * + * Inline completion providers are requested again whenever the selected item changes. + * + * The user must configure `"editor.suggest.preview": true` for this feature. + */ + readonly selectedCompletionInfo: SelectedCompletionInfoNew | undefined; + } + + // TODO@API find a better name, xyzFilter, xyzConstraint + export interface SelectedCompletionInfoNew { + range: Range; + text: string; + } + + /** + * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKindNew { + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Invoke = 0, + + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 1, + } + + /** + * @deprecated Return an array of Inline Completion items directly. Will be removed eventually. + */ + // TODO@API We could keep this and allow for `vscode.Command` instances that explain + // the result. That would replace the existing proposed menu-identifier and be more LSP friendly + // TODO@API maybe use MarkdownString + export class InlineCompletionListNew { + items: InlineCompletionItemNew[]; + + // command: Command; "Show More..." + + // description: MarkdownString + + /** + * @deprecated Return an array of Inline Completion items directly. Will be removed eventually. + */ + constructor(items: InlineCompletionItemNew[]); + } + + export class InlineCompletionItemNew { + /** + * The text to replace the range with. Must be set. + * Is used both for the preview and the accept operation. + * + * The text the range refers to must be a subword of this value (`AB` and `BEF` are subwords of `ABCDEF`, but `Ab` is not). + * Additionally, if possible, it should be a prefix of this value for a better user-experience. + * + * However, any indentation of the text to replace does not matter for the subword constraint. + * Thus, ` B` can be replaced with ` ABC`, effectively removing a whitespace and inserting `A` and `C`. + */ + insertText: string | SnippetString; + + /** + * A text that is used to decide if this inline completion should be shown. + * An inline completion is shown if the text to replace is a subword of the filter text. + */ + filterText?: string; + + /** + * The range to replace. + * Must begin and end on the same line. + * + * Prefer replacements over insertions to avoid cache invalidation: + * Instead of reporting a completion that inserts an extension at the end of a word, + * the whole word (or even the whole line) should be replaced with the extended word (or extended line) to improve the UX. + * That way, when the user presses backspace, the cache can be reused and there is no flickering. + */ + range?: Range; + + /** + * An optional {@link Command} that is executed *after* inserting this completion. + */ + command?: Command; + + // TODO@API insertText -> string | SnippetString + constructor(insertText: string, range?: Range, command?: Command); + } +} diff --git a/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts b/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts new file mode 100644 index 00000000000..aca62dc70ba --- /dev/null +++ b/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * 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/144944 + + export enum InputBoxValidationSeverity { + Info = 1, + Warning = 2, + Error = 3 + } + + export interface InputBoxOptions { + /** + * The validation message to display. This will become the new {@link InputBoxOptions#validateInput} upon finalization. + */ + validateInput2?(value: string): string | { content: string; severity: InputBoxValidationSeverity } | undefined | null | + Thenable; + } + + export interface InputBox { + /** + * The validation message to display. This will become the new {@link InputBox#validationMessage} upon finalization. + */ + validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; + } +} diff --git a/src/vscode-dts/vscode.proposed.markdownStringBaseUri.d.ts b/src/vscode-dts/vscode.proposed.markdownStringBaseUri.d.ts deleted file mode 100644 index 19a85286f67..00000000000 --- a/src/vscode-dts/vscode.proposed.markdownStringBaseUri.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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/142051 - - interface MarkdownString { - /** - * Uri that relative paths are resolved relative to. - * - * If the `baseUri` ends with `/`, it is considered a directory and relative paths in the markdown are resolved relative to that directory: - * - * ```ts - * const md = new vscode.MarkdownString(`[link](./file.js)`); - * md.baseUri = vscode.Uri.file('/path/to/dir/'); - * // Here 'link' in the rendered markdown resolves to '/path/to/dir/file.js' - * ``` - * - * If the `baseUri` is a file, relative paths in the markdown are resolved relative to the parent dir of that file: - * - * ```ts - * const md = new vscode.MarkdownString(`[link](./file.js)`); - * md.baseUri = vscode.Uri.file('/path/to/otherFile.js'); - * // Here 'link' in the rendered markdown resolves to '/path/to/file.js' - * ``` - */ - baseUri?: Uri; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts b/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts new file mode 100644 index 00000000000..44181290f93 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// https://github.com/microsoft/vscode/issues/144662 + +declare module 'vscode' { + + /** + * Describes a chaneg to a notebook cell. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentContentCellChange { + + /** + * The affected notebook. + */ + readonly cell: NotebookCell; + + /** + * The new metadata of the cell or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * The new outputs of the cell or `undefined` when they did not change. + */ + readonly outputs: readonly NotebookCellOutput[] | undefined; + + /** + * The new execution summary of the cell or `undefined` when it did not change. + */ + readonly executionSummary: NotebookCellExecutionSummary | undefined; + } + + /** + * Describes a structural change to a notebook document. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentContentChange { + + /** + * The range at which cells have been either and or removed. + */ + readonly range: NotebookRange; + + /** + * Cells that have been added to the document. + */ + readonly addedCells: readonly NotebookCell[]; + + /** + * Cells that have been removed from the document. + */ + readonly removedCells: readonly NotebookCell[]; + } + + /** + * An event describing a transactional {@link NotebookDocument notebook} change. + */ + export interface NotebookDocumentChangeEvent { + + /** + * The affected notebook. + */ + readonly notebook: NotebookDocument; + + /** + * The new metadata of the notebook or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * An array of content changes describing added and removed {@link NotebookCell cells}. + */ + readonly contentChanges: readonly NotebookDocumentContentChange[]; + + /** + * An array of {@link NotebookDocumentContentCellChange cell changes}. + */ + readonly cellChanges: readonly NotebookDocumentContentCellChange[]; + } + + export namespace workspace { + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is saved. + */ + export const onDidSaveNotebookDocument: Event; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} has changed. + */ + export const onDidChangeNotebookDocument: Event; + } +} diff --git a/src/vscode-dts/vscode.proposed.notebookDocumentSelector.d.ts b/src/vscode-dts/vscode.proposed.notebookDocumentSelector.d.ts deleted file mode 100644 index 5444140314c..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookDocumentSelector.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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/141143 - - export interface DocumentFilter { - - /** - * The {@link NotebookDocument.notebookType type} of a notebook, like `jupyter`. This allows - * to narrow down on the type of a notebook that a {@link NotebookCell.document cell document} belongs to. - * - * *Note* that combining `notebookType` and {@link DocumentFilter.scheme `scheme`} with a value - * different than `"vscode-notebook-cell"` or `undefined` is invalid and will not match - * any document. - */ - readonly notebookType?: string; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts index 74162df35c2..142e61a6a11 100644 --- a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts @@ -69,6 +69,7 @@ declare module 'vscode' { readonly viewColumn?: ViewColumn; } + /** @deprecated */ export interface NotebookDocumentMetadataChangeEvent { /** * The {@link NotebookDocument notebook document} for which the document metadata have changed. @@ -77,6 +78,7 @@ declare module 'vscode' { readonly document: NotebookDocument; } + /** @deprecated */ export interface NotebookCellsChangeData { readonly start: number; // todo@API end? Use NotebookCellRange instead? @@ -87,6 +89,7 @@ declare module 'vscode' { readonly items: NotebookCell[]; } + /** @deprecated */ export interface NotebookCellsChangeEvent { /** * The {@link NotebookDocument notebook document} for which the cells have changed. @@ -96,6 +99,7 @@ declare module 'vscode' { readonly changes: ReadonlyArray; } + /** @deprecated */ export interface NotebookCellOutputsChangeEvent { /** * The {@link NotebookDocument notebook document} for which the cell outputs have changed. @@ -106,6 +110,7 @@ declare module 'vscode' { readonly cells: NotebookCell[]; } + /** @deprecated */ export interface NotebookCellMetadataChangeEvent { /** * The {@link NotebookDocument notebook document} for which the cell metadata have changed. @@ -141,18 +146,17 @@ declare module 'vscode' { } export namespace notebooks { - - - + /** @deprecated */ export const onDidSaveNotebookDocument: Event; - + /** @deprecated */ export const onDidChangeNotebookDocumentMetadata: Event; + /** @deprecated */ export const onDidChangeNotebookCells: Event; - // todo@API add onDidChangeNotebookCellOutputs + /** @deprecated */ export const onDidChangeCellOutputs: Event; - // todo@API add onDidChangeNotebookCellMetadata + /** @deprecated */ export const onDidChangeCellMetadata: Event; } diff --git a/src/vscode-dts/vscode.proposed.outputChannelLanguage.d.ts b/src/vscode-dts/vscode.proposed.outputChannelLanguage.d.ts deleted file mode 100644 index 37f9c4a9600..00000000000 --- a/src/vscode-dts/vscode.proposed.outputChannelLanguage.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// https://github.com/microsoft/vscode/issues/19561 - -declare module 'vscode' { - - export namespace window { - - /** - * Creates a new {@link OutputChannel output channel} with the given name and language id - * If language id is not provided, then **Log** is used as default language id. - * - * **Note:** A visible or active output channel can be accessed as a {@link TextDocument text document} from {@link window.visibleTextEditors visible editors} or {@link window.activeTextEditor active editor} - * - * @param name Human-readable string which will be used to represent the channel in the UI. - * @param languageId The identifier of the language associated with the channel. - */ - export function createOutputChannel(name: string, languageId?: string): OutputChannel; - } -} diff --git a/src/vscode-dts/vscode.proposed.tabs.d.ts b/src/vscode-dts/vscode.proposed.tabs.d.ts index c4298a97a11..510d9c9fcc7 100644 --- a/src/vscode-dts/vscode.proposed.tabs.d.ts +++ b/src/vscode-dts/vscode.proposed.tabs.d.ts @@ -7,11 +7,37 @@ declare module 'vscode' { // https://github.com/Microsoft/vscode/issues/15178 - export enum TabKind { - Singular = 0, - Diff = 1, - SidebySide = 2, - Other = 3 + export class TextTabInput { + readonly uri: Uri; + constructor(uri: Uri); + } + + export class TextDiffTabInput { + readonly original: Uri; + readonly modified: Uri; + constructor(original: Uri, modified: Uri); + } + + // TODO@API remove `Editor` + export class CustomEditorTabInput { + readonly uri: Uri; + readonly viewType: string; + constructor(uri: Uri, viewType: string); + } + + // TODO@API remove `Editor` + export class NotebookEditorTabInput { + readonly uri: Uri; + readonly notebookType: string; + constructor(uri: Uri, notebookType: string); + } + + // TODO@API remove `Editor` + export class NotebookDiffEditorTabInput { + readonly original: Uri; + readonly modified: Uri; + readonly notebookType: string; + constructor(original: Uri, modified: Uri, notebookType: string); } /** @@ -24,32 +50,15 @@ declare module 'vscode' { readonly label: string; /** - * The column which the tab belongs to + * The group which the tab belongs to */ - readonly viewColumn: ViewColumn; + // TODO@API names?: `tabGroup`, `group` + readonly parentGroup: TabGroup; - /** - * The resource represented by the tab if available. - * Note: Not all tabs have a resource associated with them. - */ - readonly resource: Uri | undefined; - - /** - * The identifier of the view contained in the tab - * This is equivalent to `viewType` for custom editors and `notebookType` for notebooks. - * The built-in text editor has an id of 'default' for all configurations. - */ - readonly viewId: string | undefined; - - /** - * All the resources and viewIds represented by a tab - * {@link Tab.resource resource} and {@link Tab.viewId viewId} will - * always be at index 0. - */ - readonly additionalResourcesAndViewIds: readonly { - readonly resource: Uri | undefined; - readonly viewId: string | undefined; - }[]; + // TODO@API NAME: xyzOptions, xyzType + // TabTypeText, TabTypeTextDiff, TabTypeNotebook, TabTypeNotebookDiff, TabTypeCustom + // TabKindText, TabKindTextDiff, TabKindNotebook, TabKindNotebookDiff, TabKindCustom + readonly input: TextTabInput | TextDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | unknown; /** * Whether or not the tab is currently active @@ -63,29 +72,14 @@ declare module 'vscode' { readonly isDirty: boolean; /** - * Whether or not the tab is pinned + * Whether or not the tab is pinned (pin icon is present) */ readonly isPinned: boolean; /** - * Indicates the type of tab it is. + * Whether or not the tab is in preview mode. */ - readonly kind: TabKind; - - /** - * Moves a tab to the given index within the column. - * If the index is out of range, the tab will be moved to the end of the column. - * If the column is out of range, a new one will be created after the last existing column. - * @param index The index to move the tab to - * @param viewColumn The column to move the tab into - */ - move(index: number, viewColumn: ViewColumn): Thenable; - - /** - * Closes the tab. This makes the tab object invalid and the tab - * should no longer be used for further actions. - */ - close(): Thenable; + readonly isPreview: boolean; } export namespace window { @@ -95,20 +89,7 @@ declare module 'vscode' { export const tabGroups: TabGroups; } - interface TabGroups { - /** - * All the groups within the group container - */ - readonly all: TabGroup[]; - - /** - * An {@link Event} which fires when a group changes. - */ - onDidChangeTabGroup: Event; - - } - - interface TabGroup { + export interface TabGroup { /** * Whether or not the group is currently active */ @@ -122,11 +103,64 @@ declare module 'vscode' { /** * The active tab within the group */ + // TODO@API explain the relation between active tab groups and active tab readonly activeTab: Tab | undefined; /** * The list of tabs contained within the group */ - readonly tabs: Tab[]; + readonly tabs: readonly Tab[]; + } + + export interface TabGroups { + /** + * All the groups within the group container + */ + readonly groups: readonly TabGroup[]; + + /** + * The currently active group + */ + readonly activeTabGroup: TabGroup | undefined; + + /** + * An {@link Event} which fires when a group changes. + */ + // TODO@API add TabGroup instance + readonly onDidChangeTabGroup: Event; + + /** + * An {@link Event} which fires when a tab changes. + */ + // TODO@API use richer event type? + readonly onDidChangeTab: Event; + + /** + * An {@link Event} which fires when the active group changes. + * Whether it be which group is active. + */ + readonly onDidChangeActiveTabGroup: Event; + + /** + * Closes the tab. This makes the tab object invalid and the tab + * should no longer be used for further actions. + * @param tab The tab to close, must be reference equal to a tab given by the API + * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + */ + close(tab: Tab[], preserveFocus?: boolean): Thenable; + close(tab: Tab, preserveFocus?: boolean): Thenable; + + /** + * Moves a tab to the given index within the column. + * If the index is out of range, the tab will be moved to the end of the column. + * If the column is out of range, a new one will be created after the last existing column. + * + * @package tab The tab to move. + * @param viewColumn The column to move the tab into + * @param index The index to move the tab to + */ + // TODO@API support TabGroup in addition to ViewColumn + // TODO@API support just index for moving inside current group + move(tab: Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts new file mode 100644 index 00000000000..0af1e3157ba --- /dev/null +++ b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/142990 + + export interface TextEditorDropEvent { + /** + * The {@link TextEditor} the resource was dropped onto. + */ + readonly editor: TextEditor; + + /** + * The position in the file where the drop occurred + */ + readonly position: Position; + + /** + * The {@link DataTransfer data transfer} associated with this drop. + */ + readonly dataTransfer: DataTransfer; + + /** + * Allows to pause the event to delay apply the drop. + * + * *Note:* This function can only be called during event dispatch and not + * in an asynchronous manner: + * + * ```ts + * workspace.onWillDropOnTextEditor(event => { + * // async, will *throw* an error + * setTimeout(() => event.waitUntil(promise)); + * + * // sync, OK + * event.waitUntil(promise); + * }) + * ``` + * + * @param thenable A thenable that delays saving. + */ + waitUntil(thenable: Thenable): void; + + token: CancellationToken; + } + + export namespace workspace { + /** + * Event fired when the user drops a resource into a text editor. + */ + export const onWillDropOnTextEditor: Event; + } +} diff --git a/src/vscode-dts/vscode.proposed.timeline.d.ts b/src/vscode-dts/vscode.proposed.timeline.d.ts index ba2172a6d1e..cca46f71766 100644 --- a/src/vscode-dts/vscode.proposed.timeline.d.ts +++ b/src/vscode-dts/vscode.proposed.timeline.d.ts @@ -38,7 +38,7 @@ declare module 'vscode' { /** * The tooltip text when you hover over the timeline item. */ - detail?: string | MarkdownString | undefined; + tooltip?: string | MarkdownString | undefined; /** * The {@link Command} that should be executed when the timeline item is selected. diff --git a/src/vscode-dts/vscode.proposed.treeViewDragAndDrop.d.ts b/src/vscode-dts/vscode.proposed.treeViewDragAndDrop.d.ts deleted file mode 100644 index 6d72ac81b0e..00000000000 --- a/src/vscode-dts/vscode.proposed.treeViewDragAndDrop.d.ts +++ /dev/null @@ -1,121 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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/32592 - - /** - * A data provider that provides tree data - */ - export interface TreeDataProvider { - /** - * An optional event to signal that an element or root has changed. - * This will trigger the view to update the changed element/root and its children recursively (if shown). - * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. - */ - onDidChangeTreeData2?: Event; - } - - export interface TreeViewOptions { - /** - * An optional interface to implement drag and drop in the tree view. - */ - dragAndDropController?: TreeDragAndDropController; - } - - /** - * A class for encapsulating data transferred during a tree drag and drop event. - * - * If your `DragAndDropController` implements `handleDrag`, you can use the `value` of the `TreeDataTransferItem` - * to get back the object you put into it so long as the extension that created the `TreeDataTransferItem` runs in the same - * extension host. - */ - export class TreeDataTransferItem { - asString(): Thenable; - readonly value: any; - constructor(value: any); - } - - /** - * A map containing a mapping of the mime type of the corresponding transferred data. - * Trees that support drag and drop can implement `DragAndDropController.handleDrag` to add additional mime types - * when the drop occurs on an item in the same tree. - */ - export class TreeDataTransfer { - /** - * Retrieves the data transfer item for a given mime type. - * @param mimeType The mime type to get the data transfer item for. - */ - get(mimeType: string): T | undefined; - - /** - * Sets a mime type to data transfer item mapping. - * @param mimeType The mime type to set the data for. - * @param value The data transfer item for the given mime type. - */ - set(mimeType: string, value: T): void; - - /** - * Allows iteration through the data transfer items. - * @param callbackfn Callback for iteration through the data transfer items. - */ - forEach(callbackfn: (value: T, key: string) => void): void; - } - - /** - * Provides support for drag and drop in `TreeView`. - */ - export interface TreeDragAndDropController { - - /** - * The mime types that the `handleDrop` method of this `DragAndDropController` supports. - * This could be well-defined, existing, mime types, and also mime types defined by the extension. - * - * Each tree will automatically support drops from it's own `DragAndDropController`. To support drops from other trees, - * you will need to add the mime type of that tree. The mime type of a tree is of the format `application/vnd.code.tree.treeidlowercase`. - * - * To learn the mime type of a dragged item: - * 1. Set up your `DragAndDropController` - * 2. Use the Developer: Set Log Level... command to set the level to "Debug" - * 3. Open the developer tools and drag the item with unknown mime type over your tree. The mime types will be logged to the developer console - */ - readonly dropMimeTypes: string[]; - - /** - * The mime types that the `handleDrag` method of this `TreeDragAndDropController` may add to the tree data transfer. - * This could be well-defined, existing, mime types, and also mime types defined by the extension. - */ - readonly dragMimeTypes: string[]; - - /** - * When the user starts dragging items from this `DragAndDropController`, `handleDrag` will be called. - * Extensions can use `handleDrag` to add their `TreeDataTransferItem`s to the drag and drop. - * - * When the items are dropped on **another tree item** in **the same tree**, your `TreeDataTransferItem` objects - * will be preserved. See the documentation for `TreeDataTransferItem` for how best to take advantage of this. - * - * To add a data transfer item that can be dragged into the editor, use the application specific mime type "text/uri-list". - * The data for "text/uri-list" should be a string with `toString()`ed Uris separated by newlines. To specify a cursor position in the file, - * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number. - * - * @param source The source items for the drag and drop operation. - * @param treeDataTransfer The data transfer associated with this drag. - * @param token A cancellation token indicating that drag has been cancelled. - */ - handleDrag?(source: T[], treeDataTransfer: TreeDataTransfer, token: CancellationToken): Thenable | void; - - /** - * Called when a drag and drop action results in a drop on the tree that this `DragAndDropController` belongs too. - * - * Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed. - * - * @param source The data transfer items of the source of the drag. - * @param target The target tree element that the drop is occurring on. - * @param token A cancellation token indicating that the drop has been cancelled. - */ - handleDrop?(target: T, source: TreeDataTransfer, token: CancellationToken): Thenable | void; - } -} diff --git a/test/automation/package.json b/test/automation/package.json index 0c9f304d98c..7a7b3c5a560 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -29,7 +29,7 @@ "devDependencies": { "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", - "@types/node": "14.x", + "@types/node": "16.x", "@types/tmp": "0.2.2", "cpx2": "3.0.0", "npm-run-all": "^4.1.5", diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index e9e882e9b0a..4f0f7a94fba 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -224,7 +224,7 @@ async function launchServer(options: LaunchOptions) { ...process.env }; - const args = ['--disable-telemetry', '--port', `${port++}`, '--driver', 'web', '--extensions-dir', extensionsPath, '--server-data-dir', agentFolder, '--accept-server-license-terms']; + const args = ['--disable-telemetry', '--disable-workspace-trust', '--port', `${port++}`, '--driver', 'web', '--extensions-dir', extensionsPath, '--server-data-dir', agentFolder, '--accept-server-license-terms']; let serverLocation: string | undefined; if (codeServerPath) { @@ -282,7 +282,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) { } }); - const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b"],["skipWelcome","true"]]`; + const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","181b43c0e2949e36ecb623d8cc6de29d4fa2bae8"],["skipWelcome","true"]]`; await measureAndLog(page.goto(`${endpoint}&folder=${URI.file(workspacePath!).path}&payload=${payloadParam}`), 'page.goto()', logger); return { browser, context, page }; diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index d2311cbb4f1..b5ccab17e0f 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -21,10 +21,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.1.tgz#3b5c3a26393c19b400844ac422bd0f631a94d69d" integrity sha512-aK9jxMypeSrhiYofWWBf/T7O+KwaiAHzM4sveCdWPn71lzUSMimRnKzhXDKfKwV1kWoBo2P1aGgaIYGLf9/ljw== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/tmp@0.2.2": version "0.2.2" diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index 594808bf0d7..446c8b69e5f 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@types/mkdirp": "^1.0.1", - "@types/node": "14.x", + "@types/node": "16.x", "@types/optimist": "0.0.29", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 5281ed3ac98..0510fc4f54d 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -65,7 +65,7 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith const testExtensionUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionDevelopmentPath)).path, protocol, host, slashes: true }); const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true }); - const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b"],["skipWelcome","true"]]`; + const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","181b43c0e2949e36ecb623d8cc6de29d4fa2bae8"],["skipWelcome","true"]]`; if (path.extname(testWorkspacePath) === '.code-workspace') { await page.goto(`${endpoint.href}&workspace=${testWorkspacePath}&payload=${payloadParam}`); @@ -125,7 +125,7 @@ async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.U const root = path.join(__dirname, '..', '..', '..', '..'); const logsPath = path.join(root, '.build', 'logs', 'integration-tests-browser'); - const serverArgs = ['--driver', 'web', '--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms']; + const serverArgs = ['--driver', 'web', '--enable-proposed-api', '--disable-telemetry', '--server-data-dir', userDataDir, '--accept-server-license-terms', '--disable-workspace-trust']; let serverLocation: string; if (process.env.VSCODE_REMOTE_SERVER_PATH) { diff --git a/test/integration/browser/yarn.lock b/test/integration/browser/yarn.lock index a5682ee9a90..b3498682cd8 100644 --- a/test/integration/browser/yarn.lock +++ b/test/integration/browser/yarn.lock @@ -33,10 +33,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/optimist@0.0.29": version "0.0.29" diff --git a/test/smoke/README.md b/test/smoke/README.md index 86777cab856..e207607f329 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -41,7 +41,7 @@ yarn --cwd test/smoke #### Web There is no support for testing an old version to a new one yet. -Instead, simply configure the `--build` command line argument to point to the absolute path of the extracted server web build folder (e.g. `/vscode-server-darwin-web` for macOS). The server web build is available from the builds page (see previous subsection). +Instead, simply configure the `--build` command line argument to point to the absolute path of the extracted server web build folder (e.g. `/vscode-server-darwin-x64-web` for macOS). The server web build is available from the builds page (see previous subsection). **macOS**: if you have downloaded the server with web bits, make sure to run the following command before unzipping it to avoid security issues on startup: diff --git a/test/smoke/package.json b/test/smoke/package.json index 98e25ed8745..e0e9c671d83 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -21,7 +21,7 @@ "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", "@types/ncp": "2.0.1", - "@types/node": "14.x", + "@types/node": "16.x", "@types/node-fetch": "^2.5.10", "@types/rimraf": "3.0.2", "npm-run-all": "^4.1.5", diff --git a/test/smoke/src/areas/preferences/preferences.test.ts b/test/smoke/src/areas/preferences/preferences.test.ts index 1d8e6984a98..68a6dffdd10 100644 --- a/test/smoke/src/areas/preferences/preferences.test.ts +++ b/test/smoke/src/areas/preferences/preferences.test.ts @@ -27,7 +27,7 @@ export function setup(logger: Logger) { await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.LEFT); - await app.workbench.keybindingsEditor.updateKeybinding('workbench.action.toggleSidebarPosition', 'View: Toggle Side Bar Position', 'ctrl+u', 'Control+U'); + await app.workbench.keybindingsEditor.updateKeybinding('workbench.action.toggleSidebarPosition', 'View: Toggle Primary Side Bar Position', 'ctrl+u', 'Control+U'); await app.code.dispatchKeybinding('ctrl+u'); await app.workbench.activitybar.waitForActivityBar(ActivityBarPosition.RIGHT); diff --git a/test/smoke/yarn.lock b/test/smoke/yarn.lock index 136ea693019..7eda3e418f9 100644 --- a/test/smoke/yarn.lock +++ b/test/smoke/yarn.lock @@ -58,10 +58,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" integrity sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/rimraf@3.0.2": version "3.0.2" diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index f5dc37d1093..2c3ad1ab9fc 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -20,10 +20,6 @@ const net = require('net'); const createStatsCollector = require('mocha/lib/stats-collector'); const { applyReporter, importMochaReporter } = require('../reporter'); -// Disable render process reuse, we still have -// non-context aware native modules in the renderer. -app.allowRendererProcessReuse = false; - const optimist = require('optimist') .describe('grep', 'only run tests matching ').alias('grep', 'g').alias('grep', 'f').string('grep') .describe('run', 'only run tests from ').string('run') diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 12b9e021ca3..d59ff18c3f5 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -30,7 +30,6 @@ const excludeModules = [ 'vs/base/parts/storage/test/node/storage.test.js', // same as above, due to direct dependency to sqlite native module 'vs/workbench/contrib/testing/test/common/testResultStorage.test.js', // TODO@connor4312 https://github.com/microsoft/vscode/issues/137853 'vs/workbench/contrib/testing/test/common/testResultService.test.js', // TODO@connor4312 https://github.com/microsoft/vscode/issues/137853 - 'vs/platform/files/test/common/files.test.js' // TODO@bpasero enable once we ship Electron 16 ]; /** diff --git a/yarn.lock b/yarn.lock index e08674c4d1e..e98b58a0bde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -683,23 +683,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g== -"@electron/get@^1.0.1": - version "1.12.3" - resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.12.3.tgz#fa2723385c4b565a34c4c82f46087aa2a5fbf6d0" - integrity sha512-NFwSnVZQK7dhOYF1NQCt+HGqgL1aNdj0LUSx75uCqnZJqyiWCVdAMFV4b4/kC8HjUJAnsvdSEmjEt4G2qNQ9+Q== - dependencies: - debug "^4.1.1" - env-paths "^2.2.0" - filenamify "^4.1.0" - fs-extra "^8.1.0" - got "^9.6.0" - progress "^2.0.3" - semver "^6.2.0" - sumchecker "^3.0.1" - optionalDependencies: - global-agent "^2.0.2" - global-tunnel-ng "^2.7.1" - "@electron/get@^1.12.4": version "1.13.0" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.0.tgz#95c6bcaff4f9a505ea46792424f451efea89228c" @@ -716,6 +699,22 @@ global-agent "^2.0.2" global-tunnel-ng "^2.7.1" +"@electron/get@^1.13.0": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.13.1.tgz#42a0aa62fd1189638bd966e23effaebb16108368" + integrity sha512-U5vkXDZ9DwXtkPqlB45tfYnnYBN8PePp1z/XDCupnSpdrxT8/ThCv9WCwPLf9oqiSGZTkH6dx2jDUPuoXpjkcA== + dependencies: + debug "^4.1.1" + env-paths "^2.2.0" + fs-extra "^8.1.0" + got "^9.6.0" + progress "^2.0.3" + semver "^6.2.0" + sumchecker "^3.0.1" + optionalDependencies: + global-agent "^3.0.0" + global-tunnel-ng "^2.7.1" + "@eslint/eslintrc@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" @@ -1308,10 +1307,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-4.2.22.tgz#cf488a0f6b4a9c245d09927f4f757ca278b9c8ce" integrity sha512-LXRap3bb4AjtLZ5NOFc4ssVZrQPTgdPcNm++0SEJuJZaOA+xHkojJNYqy33A5q/94BmG5tA6yaMeD4VdCv5aSA== -"@types/node@14.x": - version "14.14.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" - integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== +"@types/node@16.x": + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/node@^10.11.7": version "10.12.21" @@ -1556,30 +1555,25 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@vscode/debugprotocol@1.54.0": - version "1.54.0" - resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.54.0.tgz#d0e687e4963e8535b94a8d549486ce25438d1a7c" - integrity sha512-/Qt6ICgqj2OzIsTJ9JJ4JTsvNz1ql+tfdbvtTWZCLJbDgkHwPNUSr6iKwCjgcZ7Hz8SbzVK8e9vIV/L7R21FCQ== - "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/ripgrep@^1.14.1": - version "1.14.1" - resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.1.tgz#66b104a6c7283d17672eb01c02b1c1294d4bb2ae" - integrity sha512-f2N/iPZhxP9bho7iK0DibJDprU+spE8hTIvQg1fi8v82oWIWU9IB4a92444GyxSaFgb+hWpQe46QkFDh5W1VpQ== +"@vscode/ripgrep@^1.14.2": + version "1.14.2" + resolved "https://registry.yarnpkg.com/@vscode/ripgrep/-/ripgrep-1.14.2.tgz#47c0eec2b64f53d8f7e1b5ffd22a62e229191c34" + integrity sha512-KDaehS8Jfdg1dqStaIPDKYh66jzKd5jy5aYEPzIv0JYFLADPsCSQPBUdsJVXnr0t72OlDcj96W05xt/rSnNFFQ== dependencies: - https-proxy-agent "^4.0.0" + https-proxy-agent "^5.0.0" proxy-from-env "^1.1.0" -"@vscode/sqlite3@4.0.12": - version "4.0.12" - resolved "https://registry.yarnpkg.com/@vscode/sqlite3/-/sqlite3-4.0.12.tgz#50b36c788b5d130c02612b27eaf6905dc2156a43" - integrity sha512-45Nbq4vgUhcejdDkX/G9K5BMMgRkBqtHtbChbvXHesMfk88USt4i94i9EM0DfHO7ijl3oIwGqzIob6lgeYi41w== +"@vscode/sqlite3@5.0.7": + version "5.0.7" + resolved "https://registry.yarnpkg.com/@vscode/sqlite3/-/sqlite3-5.0.7.tgz#358df36bb0e9e735c54785e3e4b9b2dce1d32895" + integrity sha512-NlsOf+Hir2r4zopI1qMvzWXPwPJuFscirkmFTniTAT24Yz2FWcyZxzK7UT8iSNiTqOCPz48yF55ZVHaz7tTuVQ== dependencies: - nan "2.14.2" + node-addon-api "^4.2.0" "@vscode/sudo-prompt@9.3.1": version "9.3.1" @@ -3898,12 +3892,12 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: - mimic-response "^2.0.0" + mimic-response "^3.1.0" decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: version "4.1.1" @@ -4095,10 +4089,10 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== detect-newline@^2.0.0: version "2.1.0" @@ -4303,12 +4297,12 @@ electron-to-chromium@^1.4.17: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.45.tgz#cf1144091d6683cbd45a231954a745f02fb24598" integrity sha512-czF9eYVuOmlY/vxyMQz2rGlNSjZpxNQYBe1gmQv7al171qOIhgyO9k7D5AKlgeTCSPKk+LHhj5ZyIdmEub9oNg== -electron@13.5.2: - version "13.5.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-13.5.2.tgz#5c5826e58a5e12bb5ca8047b789d07b45260ecbc" - integrity sha512-CPakwDpy5m8dL0383F5uJboQcVtn9bT/+6/wdDKo8LuTUO9aER1TF41v7feZgZW2c+UwoGPWa814ElSQ3qta2A== +electron@17.1.2: + version "17.1.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-17.1.2.tgz#b4e4a0df883d9a9854cf865efa2bb00b12d55b1d" + integrity sha512-hqKQaUIRWX5Y2eAD8FZINWD/e5TKdpkbBYbkcZmJS4Bd1PKQsaDVc9h5xoA8zZQkPymE9rss+swjRpAFurOPGQ== dependencies: - "@electron/get" "^1.0.1" + "@electron/get" "^1.13.0" "@types/node" "^14.6.2" extract-zip "^1.0.3" @@ -5122,20 +5116,6 @@ file-uri-to-path@1.0.0: resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== -filename-reserved-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" - integrity sha1-q/c9+rc10EVECr/qLZHzieu/oik= - -filenamify@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.2.0.tgz#c99716d676869585b3b5d328b3f06590d032e89f" - integrity sha512-pkgE+4p7N1n7QieOopmn3TqJaefjdWXwEkj2XLZJLKfOgcQKkn11ahvGNgTD8mLggexLiDFQxeTs14xVU22XPA== - dependencies: - filename-reserved-regex "^2.0.0" - strip-outer "^1.0.1" - trim-repeated "^1.0.0" - fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -5654,6 +5634,18 @@ global-agent@^2.0.2: semver "^7.3.2" serialize-error "^7.0.1" +global-agent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" + integrity sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== + dependencies: + boolean "^3.0.1" + es6-error "^4.1.1" + matcher "^3.0.0" + roarr "^2.15.3" + semver "^7.3.2" + serialize-error "^7.0.1" + global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -7237,13 +7229,13 @@ keygrip@~1.1.0: dependencies: tsscmp "1.0.6" -keytar@7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.6.0.tgz#498e796443cb543d31722099443f29d7b5c44100" - integrity sha512-H3cvrTzWb11+iv0NOAnoNAPgEapVZnYLVHZQyxmh7jdmVfR/c0jNNFEZ6AI38W/4DeTGTaY66ZX4Z1SbfKPvCQ== +keytar@7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== dependencies: - node-addon-api "^3.0.0" - prebuild-install "^6.0.0" + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" keyv@^3.0.0: version "3.1.0" @@ -7906,10 +7898,10 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" @@ -8149,7 +8141,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@2.14.2, nan@^2.12.1, nan@^2.13.2: +nan@^2.12.1, nan@^2.13.2: version "2.14.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== @@ -8196,15 +8188,15 @@ native-is-elevated@0.4.3: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.3.tgz#f1071c4a821acc71d43f36ff8051d3816d832e1c" integrity sha512-bHS3sCoh+raqFGIxmL/plER3eBQ+IEBy4RH/4uahhToZneTvqNKQrL0PgOTtnpL55XjBd3dy0pNtZMkCk0J48g== -native-keymap@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-3.2.1.tgz#2b34ee7e08722f107baba9beae7e61ea43eceae7" - integrity sha512-kR8r1Ody16qNE52fenuCMQBHtFpIV7HNVjQA7dm/pXpFcQmqCl8jWQuJYdZvNH7fQmyAv3lOgPfR3FDHPRYiiA== +native-keymap@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-3.3.0.tgz#927ca6afcf4ebd986b5dc1bddfab4270e750915d" + integrity sha512-nvhI1Cdr+ywhpqqGdM+JM8EjMYA1s6SW8EqhWNKtffHU07JcTKDhd8N3o0TLSFn8y2YuRFoOwfAIzdTU6TVhDA== -native-watchdog@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.3.0.tgz#88cee94c9dc766b85c8506eda14c8bd8c9618e27" - integrity sha512-WOjGRNGkYZ5MXsntcvCYrKtSYMaewlbCFplbcUVo9bE80LPVt8TAVFHYWB8+a6fWCGYheq21+Wtt6CJrUaCJhw== +native-watchdog@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.4.0.tgz#547a1f9f88754c38089c622d405ed1e324c7a545" + integrity sha512-4FynAeGtTpoQ2+5AxVJXGEGsOzPsNYDh8Xmawjgs7YWJe+bbbgt7CYlA/Qx6X+kwtN5Ey1aNSm9MqZa0iNKkGw== natural-compare@^1.4.0: version "1.4.0" @@ -8247,23 +8239,33 @@ nise@^5.1.0: just-extend "^4.0.2" path-to-regexp "^1.7.0" -node-abi@^2.21.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== +node-abi@^3.3.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32" + integrity sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw== dependencies: - semver "^5.4.1" - -node-addon-api@^3.0.0, node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + semver "^7.3.5" node-addon-api@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== +node-addon-api@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-addon-api@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87" + integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q== + +node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -8982,6 +8984,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -9477,40 +9484,30 @@ postcss-value-parser@^4.0.2: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== +"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== dependencies: - chalk "^2.4.2" + picocolors "^0.2.1" source-map "^0.6.1" - supports-color "^6.1.0" -postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17" - integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ== +prebuild-install@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" + integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg== dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - -prebuild-install@^6.0.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== - dependencies: - detect-libc "^1.0.3" + 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 "^2.21.0" + node-abi "^3.3.0" npmlog "^4.0.1" pump "^3.0.0" rc "^1.2.7" - simple-get "^3.0.3" + simple-get "^4.0.0" tar-fs "^2.0.0" tunnel-agent "^0.6.0" @@ -10391,12 +10388,12 @@ simple-concat@^1.0.0: resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: - decompress-response "^4.2.0" + decompress-response "^6.0.0" once "^1.3.1" simple-concat "^1.0.0" @@ -10952,13 +10949,6 @@ strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strip-outer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" - integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== - dependencies: - escape-string-regexp "^1.0.2" - style-loader@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.0.0.tgz#1d5296f9165e8e2c85d24eee0b7caf9ec8ca1f82" @@ -11009,13 +10999,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -11394,17 +11377,10 @@ tree-kill@^1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -trim-repeated@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" - integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= - dependencies: - escape-string-regexp "^1.0.2" - -ts-loader@^9.2.3: - version "9.2.3" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.3.tgz#dc3b6362a4d4382493cd4f138d345f419656de68" - integrity sha512-sEyWiU3JMHBL55CIeC4iqJQadI0U70A5af0kvgbNLHVNz2ACztQg0j/9x10bjjIht8WfFYLKfn4L6tkZ+pu+8Q== +ts-loader@^9.2.7: + version "9.2.7" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.7.tgz#948654099ca96992b62ec47bd9cee5632006e101" + integrity sha512-Fxh44mKli9QezgbdCXkEJWxnedQ0ead7DXTH+lfXEPedu+Y9EtMJ2aQ9G3Dj1j7Q612E8931rww8NDZha4Tibg== dependencies: chalk "^4.1.0" enhanced-resolve "^5.0.0" @@ -11541,10 +11517,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.6.0-dev.20220209: - version "4.6.0-dev.20220209" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.0-dev.20220209.tgz#e50fbf9838cd1d102398c360dbc7d5e2b17be4d9" - integrity sha512-T55LJkTXli4n2+zjAkxTXXM2aCUErlEnJGcNInZh/GWCwlEKWHWRDZ8d7Ow1NSGUGwJHjb/cnESptrsCP5p8PA== +typescript@^4.7.0-dev.20220316: + version "4.7.0-dev.20220316" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.0-dev.20220316.tgz#255c8079e39df6182af0089fad6092a8ea49344a" + integrity sha512-026xY5jsAZ/uBqzY2wI28y+iTVHQH+V5Ws2FLIIsusz+ARgsLKF74nz+9JY5O/OQR4/fVB/ky1etIlEo3Tu7tw== typical@^4.0.0: version "4.0.0" @@ -12354,15 +12330,15 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-search@0.9.0-beta.10: - version "0.9.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.10.tgz#80677a5e105d4410feae92b90fcdd5b538067070" - integrity sha512-fxKwbsu+ZNgZ689sAX1PHhWAW+8/abAGD8B7SMWwelKhJmbRybHoaLAYCeUrZJlJHljwjgW3Ptk7OpONNydh1A== +xterm-addon-search@0.9.0-beta.11: + version "0.9.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.11.tgz#e6af723903e470564682eb80e5f0ca7e5d291c60" + integrity sha512-sUaOrgqFJvN7oI20ruKVOEqN5t4UK9q/pAR7FSjH5vVl6h0Hhtndh9bhrgoKCSe+Do1YfZCcpWx0Sbs9Y+2Ptg== -xterm-addon-serialize@0.7.0-beta.9: - version "0.7.0-beta.9" - resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.9.tgz#122dcc1b764fad1a9970079c690e0810c7936a2d" - integrity sha512-a9DIC624nmJKcY8e8ykzib9Lefli/ea/87JFtUBDZPe5w12NM0XlQgDkcyCS1MhcTQKA1RYoEp9003QDkmIybQ== +xterm-addon-serialize@0.7.0-beta.11: + version "0.7.0-beta.11" + resolved "https://registry.yarnpkg.com/xterm-addon-serialize/-/xterm-addon-serialize-0.7.0-beta.11.tgz#d9d6a862b3d70fc6160e39bc235a2a9ef45e20d6" + integrity sha512-okirLeH8VpOxlnZkER2lN0emaTgWe/DHVGpY9ByfLIoYw506Ci2LRcOjyYwJBRTJcfLp3TGnwQSYfooCZHDndw== xterm-addon-unicode11@0.4.0-beta.3: version "0.4.0-beta.3" @@ -12374,15 +12350,15 @@ xterm-addon-webgl@0.12.0-beta.24: resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.24.tgz#5c17256933991856554c95c9bd1eaab42e9727a0" integrity sha512-+wZxKReEOlfN9JRHyikoffA6Do61/THR7QY35ajkQo0lLutKr6hTd/TLTuZh0PhFVelgTgudpXqlP++Lc0WFIA== -xterm-headless@4.18.0-beta.15: - version "4.18.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.18.0-beta.15.tgz#f710959af0aea37f8395a8011e431f2341405473" - integrity sha512-uIrWvVSdZzDneDPD9/u4mZ+OvnPDpsI/bvUzQ923/9GEyLO2UlJGipJUBhMB5W+xhox5PZJIAJJxCHnXo8Vx2Q== +xterm-headless@4.19.0-beta.7: + version "4.19.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.7.tgz#82de331ec183bfe8758250617b1dff700dc9380a" + integrity sha512-wLzw3Kro1UYXLd4ytk7mISrj7IytEAVCHEuk+Cdckknh+HiX1zFU351uOOh+IqpElIbibgor8kqB5ZDuptfaYw== -xterm@4.18.0-beta.15: - version "4.18.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.18.0-beta.15.tgz#99d40cfbd2e7f3343b2a125fd7d4b3bb864ca2f5" - integrity sha512-e3JkreKDjXNZcpQsHybaroTGXTtq7Lu1Bx+wuviBBllhz9CxI+uHzwMNHPgdFaZ+zwJq85hyeVHn354ooJ8nzA== +xterm@4.19.0-beta.7: + version "4.19.0-beta.7" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.7.tgz#25165366e005876d1e11418989b88687530ad902" + integrity sha512-BusEhdm+7Dwhtilk67mEISfYzrwJYXLgN+N+jbwVPZqDR9/CwPoG/cq6InibLAvciK1JCBwzSB32XeHFtZskWA== y18n@^3.2.1: version "3.2.2"