diff --git a/.eslintrc.json b/.eslintrc.json index 22157840d84..da169cdcc0c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -248,6 +248,7 @@ "url", "util", "v8-inspect-profiler", + "vscode-policy-watcher", "vscode-proxy-agent", "vscode-regexpp", "vscode-textmate", diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml new file mode 100644 index 00000000000..0d94b83350a --- /dev/null +++ b/.github/workflows/basic.yml @@ -0,0 +1,184 @@ +name: Basic checks + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + main: + if: github.ref != 'refs/heads/main' + name: Compilation, Unit and Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + # TODO: rename azure-pipelines/linux/xvfb.init to github-actions + - name: Setup Build Environment + run: | + sudo apt-get update + sudo apt-get install -y libxkbfile-dev pkg-config libsecret-1-dev libxss1 dbus xvfb libgtk-3-0 libgbm1 + sudo cp build/azure-pipelines/linux/xvfb.init /etc/init.d/xvfb + sudo chmod +x /etc/init.d/xvfb + sudo update-rc.d xvfb defaults + sudo service xvfb start + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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@v3 + with: + path: "**/node_modules" + 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' }} + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache yarn directory + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + uses: actions/cache@v3 + 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: Compile and Download + run: yarn npm-run-all --max_old_space_size=4095 -lp compile "electron x64" playwright-install download-builtin-extensions + + - name: Compile Integration Tests + run: yarn --cwd test/integration/browser compile + + - name: Run Unit Tests + id: electron-unit-tests + run: DISPLAY=:10 ./scripts/test.sh + + - name: Run Integration Tests (Electron) + id: electron-integration-tests + run: DISPLAY=:10 ./scripts/test-integration.sh + + hygiene: + if: github.ref != 'refs/heads/main' + name: Hygiene and Layering + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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@v3 + with: + path: "**/node_modules" + 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' }} + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache yarn directory + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + uses: actions/cache@v3 + 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 Hygiene Checks + run: yarn gulp hygiene + + - name: Run Valid Layers Checks + run: yarn valid-layers-check + + - name: Compile /build/ + run: yarn --cwd build compile + + - name: Check clean git state + run: ./.github/workflows/check-clean-git-state.sh + + - name: Run eslint + run: yarn eslint + + - name: Run vscode-dts Compile Checks + run: yarn vscode-dts-compile-check + + - name: Run Trusted Types Checks + run: yarn tsec-compile-check + + warm-cache: + name: Warm up node modules cache + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 40 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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@v3 + with: + path: "**/node_modules" + 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' }} + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Cache yarn directory + if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} + uses: actions/cache@v3 + 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 diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 98a739f8c7f..af70c86caa0 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -28,9 +28,11 @@ jobs: uses: ./actions/new-release with: label: new release + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} labelColor: "006b75" labelDescription: Issues found in a recent release of VS Code + oldVersionMessage: "Thanks for creating this issue! It looks like you may be using an old version of VS Code, the latest stable release is {currentVersion}. Please try upgrading to the latest version and checking whether this issue remains.\n\nHappy Coding!" days: 5 - name: Run Clipboard Labeler diff --git a/.github/workflows/pr-chat.yml b/.github/workflows/pr-chat.yml index 95cee4ed196..13803fda778 100644 --- a/.github/workflows/pr-chat.yml +++ b/.github/workflows/pr-chat.yml @@ -1,11 +1,12 @@ name: PR Chat on: pull_request_target: - types: [opened, ready_for_review] + types: [opened, ready_for_review, closed] jobs: main: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - name: Checkout Actions uses: actions/checkout@v2 @@ -20,4 +21,5 @@ jobs: with: token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} slack_token: ${{ secrets.SLACK_TOKEN }} + slack_bot_name: "VSCodeBot" notification_channel: codereview diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 90cc4971f43..48195c79000 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:\"April 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\r\n\r\n$MILESTONE=milestone:\"May 2022\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index e2356a2d8a9..4ce504b017f 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:\"April 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:\"May 2022\"\n\n$MINE=assignee:@me" }, { "kind": 1, diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index b4a61ec261c..7015a401be5 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -12,7 +12,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev 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-emmet-helper repo:microsoft/vscode-jupyter repo:microsoft/vscode-python\n$milestone=milestone:\"March 2022\"" + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-dev 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-emmet-helper repo:microsoft/vscode-jupyter repo:microsoft/vscode-python\n$milestone=milestone:\"May 2022\"" }, { "kind": 1, diff --git a/.vscode/notebooks/vscode-dev.github-issues b/.vscode/notebooks/vscode-dev.github-issues index 2178fa29d59..9266fd7654c 100644 --- a/.vscode/notebooks/vscode-dev.github-issues +++ b/.vscode/notebooks/vscode-dev.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-dev milestone:\"December 2021\" is:open" + "value": "repo:microsoft/vscode-dev milestone:\"May 2022\" is:open" }, { "kind": 2, @@ -32,11 +32,11 @@ { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-remote-repositories-github milestone:\"December 2021\" is:open" + "value": "repo:microsoft/vscode-remote-repositories-github milestone:\"May 2022\" is:open" }, { "kind": 2, "language": "github-issues", - "value": "repo:microsoft/vscode-remotehub milestone:\"December 2021\" is:open" + "value": "repo:microsoft/vscode-remotehub milestone:\"May 2022\" is:open" } -] +] \ No newline at end of file diff --git a/.yarnrc b/.yarnrc index 95494af5201..f2811eb170a 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,4 +1,4 @@ disturl "https://electronjs.org/headers" -target "17.4.3" +target "17.4.4" runtime "electron" build_from_source "true" diff --git a/build/.moduleignore b/build/.moduleignore index 22c1b4fe48f..a2e1b715e1b 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -107,6 +107,14 @@ vscode-encrypt/binding.gyp vscode-encrypt/README.md !vscode-encrypt/build/Release/vscode-encrypt-native.node +vscode-policy-watcher/build/** +vscode-policy-watcher/.husky/** +vscode-policy-watcher/src/** +vscode-policy-watcher/binding.gyp +vscode-policy-watcher/README.md +vscode-policy-watcher/index.d.ts +!vscode-policy-watcher/build/Release/vscode-policy-watcher.node + vscode-windows-ca-certs/**/* !vscode-windows-ca-certs/package.json !vscode-windows-ca-certs/**/*.node diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml index 773b5a40845..dd495426b6d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -1,6 +1,15 @@ parameters: - name: VSCODE_QUALITY type: string + - name: VSCODE_RUN_UNIT_TESTS + type: boolean + default: true + - name: VSCODE_RUN_INTEGRATION_TESTS + type: boolean + default: true + - name: VSCODE_RUN_SMOKE_TESTS + type: boolean + default: true steps: - task: NodeTool@0 @@ -165,91 +174,126 @@ steps: VSCODE_ARCH=$(VSCODE_ARCH) DEBUG=electron-osx-sign* node build/darwin/sign.js displayName: Set Hardened Entitlements - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 - - script: | - set -e - yarn test-node --build - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + yarn test-node --build + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 - - script: | - set -e - DEBUG=*browser* yarn test-browser-no-install --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium & Webkit) - timeoutInMinutes: 30 + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + DEBUG=*browser* yarn test-browser-no-install --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium & Webkit) + timeoutInMinutes: 30 - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - 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_ARCH)" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + yarn gulp \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-custom-editor-tests \ + compile-extension:vscode-notebook-tests \ + compile-extension:vscode-test-resolver + displayName: Build integration tests + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - 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 + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + set -e + 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_ARCH)" \ + ./scripts/test-integration.sh --build --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 - - script: | - set -e - 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_ARCH)" \ - ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + 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 - - script: | - set -e - ps -ef - displayName: Diagnostics before smoke test run - continueOnError: true - condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + 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_ARCH)" \ + ./scripts/test-remote-integration.sh + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --tracing --headless - timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + ps -ef + displayName: Diagnostics before smoke test run + continueOnError: true + condition: succeededOrFailed() - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --web --tracing --headless + timeoutInMinutes: 20 + displayName: Run smoke tests (Browser, Chromium) - - script: | - 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_ARCH)" \ - yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) - - script: | - set -e - ps -ef - displayName: Diagnostics after smoke test run - continueOnError: true - condition: succeededOrFailed() + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + yarn gulp compile-extension:vscode-test-resolver + 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_ARCH)" \ + yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) + + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + ps -ef + displayName: Diagnostics after smoke test run + continueOnError: true + condition: succeededOrFailed() - task: PublishPipelineArtifact@0 inputs: diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index 4b82e42689c..7dba0eef378 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -1,6 +1,15 @@ parameters: - name: VSCODE_QUALITY type: string + - name: VSCODE_RUN_UNIT_TESTS + type: boolean + default: true + - name: VSCODE_RUN_INTEGRATION_TESTS + type: boolean + default: true + - name: VSCODE_RUN_SMOKE_TESTS + type: boolean + default: true steps: - task: NodeTool@0 @@ -113,16 +122,6 @@ steps: - script: | set -e export npm_config_arch=$(NPM_ARCH) - # node-gyp@9.0.0 shipped with node@16.15.0 starts using config.gypi - # from the custom headers path if dist-url option was set instead of - # using the config value from the process. Electron builds with pointer compression - # enabled for x64 and arm64, but incorrectly ships a single copy of config.gypi - # with v8_enable_pointer_compression option always set for all target architectures. - # We use the force_process_config option to use the config.gypi from the - # nodejs process executing npm for 32-bit architectures. - if [ "$NPM_ARCH" = "armv7l" ]; then - export npm_config_force_process_config="true" - fi if [ -z "$CC" ] || [ -z "$CXX" ]; then # Download clang based on chromium revision used by vscode @@ -221,104 +220,139 @@ steps: stat $ELECTRON_ROOT/chrome-sandbox displayName: Change setuid helper binary permission - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - yarn test-node --build - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + yarn test-node --build + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - DEBUG=*browser* yarn test-browser-no-install --build --browser chromium --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - script: | + set -e + DEBUG=*browser* yarn test-browser-no-install --build --browser chromium --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium) + timeoutInMinutes: 15 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - INTEGRATION_TEST_APP_NAME="$APP_NAME" \ - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + yarn gulp \ + compile-extension:css-language-features-server \ + compile-extension:emmet \ + compile-extension:git \ + compile-extension:github-authentication \ + compile-extension:html-language-features-server \ + compile-extension:ipynb \ + compile-extension:json-language-features-server \ + compile-extension:markdown-language-features \ + compile-extension-media \ + compile-extension:microsoft-authentication \ + compile-extension:typescript-language-features \ + compile-extension:vscode-api-tests \ + compile-extension:vscode-colorize-tests \ + compile-extension:vscode-custom-editor-tests \ + compile-extension:vscode-notebook-tests \ + compile-extension:vscode-test-resolver + displayName: Build integration tests + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ - ./scripts/test-web-integration.sh --browser chromium - displayName: Run integration tests (Browser, Chromium) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + ./scripts/test-integration.sh --build --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") - INTEGRATION_TEST_APP_NAME="$APP_NAME" \ - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ + ./scripts/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser, Chromium) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - ps -ef - cat /proc/sys/fs/inotify/max_user_watches - lsof | wc -l - displayName: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + APP_NAME=$(node -p "require(\"$APP_ROOT/resources/app/product.json\").applicationName") + INTEGRATION_TEST_APP_NAME="$APP_NAME" \ + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + ./scripts/test-remote-integration.sh + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" - timeoutInMinutes: 20 - displayName: Run smoke tests (Browser, Chromium) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + displayName: Diagnostics before smoke test run (processes, max_user_watches, number of opened file handles) + continueOnError: true + condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - yarn smoketest-no-compile --tracing --build "$APP_PATH" - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --web --tracing --headless --electronArgs="--disable-dev-shm-usage" + timeoutInMinutes: 20 + displayName: Run smoke tests (Browser, Chromium) + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" - timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + yarn smoketest-no-compile --tracing --build "$APP_PATH" + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | - set -e - ps -ef - cat /proc/sys/fs/inotify/max_user_watches - lsof | wc -l - displayName: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + yarn gulp compile-extension:vscode-test-resolver + APP_PATH=$(agent.builddirectory)/VSCode-linux-$(VSCODE_ARCH) + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-linux-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --tracing --remote --build "$APP_PATH" + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - script: | + set -e + ps -ef + cat /proc/sys/fs/inotify/max_user_watches + lsof | wc -l + displayName: Diagnostics after smoke test run (processes, max_user_watches, number of opened file handles) + continueOnError: true + condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishPipelineArtifact@0 inputs: diff --git a/build/azure-pipelines/product-build-pr.yml b/build/azure-pipelines/product-build-pr.yml index db20311683d..3c45858413e 100644 --- a/build/azure-pipelines/product-build-pr.yml +++ b/build/azure-pipelines/product-build-pr.yml @@ -1,4 +1,6 @@ -trigger: none +trigger: + - main + - release/* pr: branches: @@ -19,7 +21,7 @@ variables: - name: skipComponentGovernanceDetection value: true - name: ENABLE_TERRAPIN - value: true + value: false - name: VSCODE_PUBLISH value: false - name: VSCODE_QUALITY @@ -31,8 +33,7 @@ stages: - stage: Compile jobs: - job: Compile - pool: - vmImage: ubuntu-18.04 + pool: vscode-1es-vscode-linux-18.04 variables: VSCODE_ARCH: x64 steps: @@ -42,8 +43,7 @@ stages: - stage: LinuxServerDependencies dependsOn: [] - pool: - vmImage: ubuntu-18.04 + pool: vscode-1es-vscode-linux-18.04 jobs: - job: x64 container: centos7-devtoolset8-x64 @@ -58,10 +58,10 @@ stages: - stage: Windows dependsOn: - Compile - pool: - vmImage: windows-2019 + pool: vscode-1es-vscode-windows-2019 jobs: - - job: Windows + - job: WindowsUnitTests + displayName: Unit Tests timeoutInMinutes: 120 variables: VSCODE_ARCH: x64 @@ -69,15 +69,42 @@ stages: - template: win32/product-build-win32.yml parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: WindowsIntegrationTests + displayName: Integration Tests + timeoutInMinutes: 120 + variables: + VSCODE_ARCH: x64 + steps: + - template: win32/product-build-win32.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: WindowsSmokeTests + displayName: Smoke Tests + timeoutInMinutes: 120 + variables: + VSCODE_ARCH: x64 + steps: + - template: win32/product-build-win32.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true - stage: Linux dependsOn: - Compile - LinuxServerDependencies - pool: - vmImage: ubuntu-18.04 + pool: vscode-1es-vscode-linux-18.04 jobs: - - job: Linuxx64 + - job: Linuxx64UnitTest + displayName: Unit Tests container: vscode-bionic-x64 variables: VSCODE_ARCH: x64 @@ -87,6 +114,37 @@ stages: - template: linux/product-build-linux-client.yml parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64IntegrationTest + displayName: Integration Tests + container: vscode-bionic-x64 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-client.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: Linuxx64SmokeTest + displayName: Smoke Tests + container: vscode-bionic-x64 + variables: + VSCODE_ARCH: x64 + NPM_ARCH: x64 + DISPLAY: ":10" + steps: + - template: linux/product-build-linux-client.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true - stage: macOS dependsOn: @@ -96,7 +154,8 @@ stages: variables: BUILDSECMON_OPT_IN: true jobs: - - job: macOSTest + - job: macOSUnitTest + displayName: Unit Tests timeoutInMinutes: 90 variables: VSCODE_ARCH: x64 @@ -104,3 +163,30 @@ stages: - template: darwin/product-build-darwin-test.yml parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: true + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: false + - job: macOSIntegrationTest + displayName: Integration Tests + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: darwin/product-build-darwin-test.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: true + VSCODE_RUN_SMOKE_TESTS: false + - job: macOSSmokeTest + displayName: Smoke Tests + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: darwin/product-build-darwin-test.yml + parameters: + VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} + VSCODE_RUN_UNIT_TESTS: false + VSCODE_RUN_INTEGRATION_TESTS: false + VSCODE_RUN_SMOKE_TESTS: true diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index d089f70d228..7501869b57d 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -1,6 +1,15 @@ parameters: - name: VSCODE_QUALITY type: string + - name: VSCODE_RUN_UNIT_TESTS + type: boolean + default: true + - name: VSCODE_RUN_INTEGRATION_TESTS + type: boolean + default: true + - name: VSCODE_RUN_SMOKE_TESTS + type: boolean + default: true steps: - task: NodeTool@0 @@ -94,14 +103,6 @@ steps: . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" $env:npm_config_arch="$(VSCODE_ARCH)" - # node-gyp@9.0.0 shipped with node@16.15.0 starts using config.gypi - # from the custom headers path if dist-url option was set instead of - # using the config value from the process. Electron builds with pointer compression - # enabled for x64 and arm64, but incorrectly ships a single copy of config.gypi - # with v8_enable_pointer_compression option always set for all target architectures. - # We use the force_process_config option to use the config.gypi from the - # nodejs process executing npm for 32-bit architectures. - if ('$(VSCODE_ARCH)' -eq 'ia32') { $env:npm_config_force_process_config="true" } $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile --check-files } } env: @@ -135,13 +136,6 @@ steps: displayName: Download Electron condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { node build\lib\policies } - displayName: Generate Group Policy definitions - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" @@ -184,105 +178,142 @@ steps: displayName: Download Playwright condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn electron $(VSCODE_ARCH) } - exec { .\scripts\test.bat --build --tfs "Unit Tests" } - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn electron $(VSCODE_ARCH) } + exec { .\scripts\test.bat --build --tfs "Unit Tests" } + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn test-node --build } - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn test-node --build } + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { yarn test-browser-no-install --sequential --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } - displayName: Run unit tests (Browser, Chromium & Firefox) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_UNIT_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn test-browser-no-install --sequential --build --browser chromium --browser firefox --tfs "Browser Unit Tests" } + displayName: Run unit tests (Browser, Chromium & Firefox) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn gulp ` + compile-extension:css-language-features-server ` + compile-extension:emmet ` + compile-extension:git ` + compile-extension:github-authentication ` + compile-extension:html-language-features-server ` + compile-extension:ipynb ` + compile-extension:json-language-features-server ` + compile-extension:markdown-language-features ` + compile-extension-media ` + compile-extension:microsoft-authentication ` + compile-extension:typescript-language-features ` + compile-extension:vscode-api-tests ` + compile-extension:vscode-colorize-tests ` + compile-extension:vscode-custom-editor-tests ` + compile-extension:vscode-notebook-tests ` + compile-extension:vscode-test-resolver ` + } + displayName: Build integration tests + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\scripts\test-web-integration.bat --browser firefox } - displayName: Run integration tests (Browser, Firefox) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - powershell: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-integration.bat --build --tfs "Integration Tests" } + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-remote-integration.bat } - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\scripts\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser, Firefox) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - exec {.\build\azure-pipelines\win32\listprocesses.bat } - displayName: Diagnostics before smoke test run - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_INTEGRATION_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\scripts\test-remote-integration.bat } + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --web --tracing --headless } - displayName: Run smoke tests (Browser, Chromium) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + exec {.\build\azure-pipelines\win32\listprocesses.bat } + displayName: Diagnostics before smoke test run + continueOnError: true + condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --tracing --build "$AppRoot" } - displayName: Run smoke tests (Electron) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)" + exec { yarn smoketest-no-compile --web --tracing --headless } + displayName: Run smoke tests (Browser, Chromium) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" - exec { yarn smoketest-no-compile --tracing --remote --build "$AppRoot" } - displayName: Run smoke tests (Remote) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + exec { yarn smoketest-no-compile --tracing --build "$AppRoot" } + displayName: Run smoke tests (Electron) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - - powershell: | - . build/azure-pipelines/win32/exec.ps1 - exec {.\build\azure-pipelines\win32\listprocesses.bat } - displayName: Diagnostics after smoke test run - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)" + exec { yarn gulp compile-extension:vscode-test-resolver } + exec { yarn smoketest-no-compile --tracing --remote --build "$AppRoot" } + displayName: Run smoke tests (Remote) + timeoutInMinutes: 20 + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + + - ${{ if eq(parameters.VSCODE_RUN_SMOKE_TESTS, true) }}: + - powershell: | + . build/azure-pipelines/win32/exec.ps1 + exec {.\build\azure-pipelines\win32\listprocesses.bat } + displayName: Diagnostics after smoke test run + continueOnError: true + condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishPipelineArtifact@0 inputs: diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 7b3a5043154..268650959cd 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -65,7 +65,6 @@ const vscodeResources = [ 'out-build/vs/base/browser/ui/codicons/codicon/**', 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', 'out-build/vs/platform/environment/node/userDataPath.js', - 'out-build/vs/platform/extensions/node/extensionHostStarterWorkerMain.js', 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', @@ -289,6 +288,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); } else if (platform === 'darwin') { const shortcut = gulp.src('resources/darwin/bin/code.sh') + .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename('bin/code')); all = es.merge(all, shortcut); @@ -331,13 +331,10 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); - result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) - .pipe(rename(f => f.dirname = `policies/${f.dirname}`))); - } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@PRODNAME@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@APPNAME@@', product.applicationName)) .pipe(rename('bin/' + product.applicationName))); } diff --git a/build/hygiene.js b/build/hygiene.js index a5d2b080172..1668e508ec9 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -54,7 +54,7 @@ function hygiene(some, linting = true) { const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); if (m) { console.error( - file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}". To suppress, use // allow-any-unicode-next-line` + file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` ); errorCount++; } diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 221e5ba7516..2671a5a3a6c 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -97,7 +97,7 @@ function writeControlFile(control) { fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } function getBuiltInExtensions() { - log('Syncronizing built-in extensions...'); + log('Synchronizing built-in extensions...'); log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); const control = readControlFile(); const streams = []; diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index f5a03d1304e..1abc85b3b09 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -136,7 +136,7 @@ function writeControlFile(control: IControlFile): void { } export function getBuiltInExtensions(): Promise { - log('Syncronizing built-in extensions...'); + log('Synchronizing built-in extensions...'); log(`You can manage built-in extensions with the ${ansiColors.cyan('--builtin')} flag`); const control = readControlFile(); diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 06bfbb2b506..b7b2cd1833e 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -119,7 +119,11 @@ "project": "vscode-workbench" }, { - "name": "vs/workbench/contrib/localizations", + "name": "vs/workbench/contrib/mergeEditor", + "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/localization", "project": "vscode-workbench" }, { diff --git a/cgmanifest.json b/cgmanifest.json index a4a8dc1be73..d153e97eb0f 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "322f1c3f8907f2592eef5b5e03a97045e30df9e3" + "commitHash": "085a15fd95969f3c61a52b39d64a7048d306dabe" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "17.4.3" + "version": "17.4.4" }, { "component": { diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts index 0cfde9025f3..8fa2d81bd03 100644 --- a/extensions/css-language-features/client/src/browser/cssClientMain.ts +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionContext, Uri } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; +import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../cssClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -15,8 +15,10 @@ declare const TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string }; }; +let client: BaseLanguageClient | undefined; + // this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/cssServerMain.js'); try { const worker = new Worker(serverMain.toString()); @@ -24,9 +26,17 @@ export function activate(context: ExtensionContext) { return new LanguageClient(id, name, clientOptions, worker); }; - startClient(context, newLanguageClient, { TextDecoder }); + client = await startClient(context, newLanguageClient, { TextDecoder }); } catch (e) { console.log(e); } } + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } +} + diff --git a/extensions/css-language-features/client/src/cssClient.ts b/extensions/css-language-features/client/src/cssClient.ts index 282c347bdd8..6f6238465d8 100644 --- a/extensions/css-language-features/client/src/cssClient.ts +++ b/extensions/css-language-features/client/src/cssClient.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 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 { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, BaseLanguageClient, DocumentRangeFormattingParams, DocumentRangeFormattingRequest } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; import { getCustomDataSource } from './customData'; import { RequestService, serveFileSystemRequests } from './requests'; @@ -15,7 +15,7 @@ namespace CustomDataChangedNotification { const localize = nls.loadMessageBundle(); -export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; +export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; @@ -39,7 +39,7 @@ interface CSSFormatSettings { const cssFormatSettingKeys: (keyof CSSFormatSettings)[] = ['newlineBetweenSelectors', 'newlineBetweenRules', 'spaceAroundSelectorSeparator', 'braceStyle', 'preserveNewLines', 'maxPreserveNewLines']; -export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { +export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const customDataSource = getCustomDataSource(context.subscriptions); @@ -100,31 +100,25 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua // Create the language client and start the client. let client = newLanguageClient('css', localize('cssserver.name', 'CSS Language Server'), clientOptions); client.registerProposedFeatures(); - client.onReady().then(() => { + await client.start(); + + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); - customDataSource.onDidChange(() => { - 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); }); - let disposable = client.start(); - // Push the disposable to the context's subscriptions so that the - // client can be deactivated on extension deactivation - context.subscriptions.push(disposable); + // 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))); + } - client.onReady().then(() => { - context.subscriptions.push(initCompletionProvider()); - }); + serveFileSystemRequests(client, runtime); + + + context.subscriptions.push(initCompletionProvider()); function initCompletionProvider(): Disposable { const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/; @@ -204,11 +198,10 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } } } - console.log(JSON.stringify(params.options)); return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( client.protocol2CodeConverter.asTextEdits, (error) => { - client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); + client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []); return Promise.resolve([]); } ); @@ -216,4 +209,6 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua }); } } + + return client; } diff --git a/extensions/css-language-features/client/src/node/cssClientMain.ts b/extensions/css-language-features/client/src/node/cssClientMain.ts index b88838d8912..dfbe121f822 100644 --- a/extensions/css-language-features/client/src/node/cssClientMain.ts +++ b/extensions/css-language-features/client/src/node/cssClientMain.ts @@ -6,11 +6,14 @@ import { getNodeFSRequestService } from './nodeFs'; import { ExtensionContext, extensions } from 'vscode'; import { startClient, LanguageClientConstructor } from '../cssClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; + +let client: BaseLanguageClient | undefined; + // this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { const clientMain = extensions.getExtension('vscode.css-language-features')?.packageJSON?.main || ''; const serverMain = `./server/${clientMain.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/cssServerMain`; @@ -30,5 +33,12 @@ export function activate(context: ExtensionContext) { return new LanguageClient(id, name, serverOptions, clientOptions); }; - startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder }); + client = await startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder }); +} + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } } diff --git a/extensions/css-language-features/client/src/requests.ts b/extensions/css-language-features/client/src/requests.ts index 6aea3ab8fad..f19918e57ef 100644 --- a/extensions/css-language-features/client/src/requests.ts +++ b/extensions/css-language-features/client/src/requests.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Uri, workspace } from 'vscode'; -import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; +import { RequestType, BaseLanguageClient } from 'vscode-languageclient'; import { Runtime } from './cssClient'; export namespace FsContentRequest { @@ -18,7 +18,7 @@ export namespace FsReadDirRequest { export const type: RequestType = new RequestType('fs/readDir'); } -export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) { +export function serveFileSystemRequests(client: BaseLanguageClient, runtime: Runtime) { client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string }) => { const uri = Uri.parse(param.uri); if (uri.scheme === 'file' && runtime.fs) { diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index e9ea587d4db..c4955243c8f 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -336,7 +336,10 @@ "type": "string", "scope": "resource", "default": "collapse", - "enum": ["collapse", "expand"], + "enum": [ + "collapse", + "expand" + ], "markdownDescription": "%css.format.braceStyle.desc%" }, "css.format.preserveNewLines": { @@ -638,7 +641,10 @@ "type": "string", "scope": "resource", "default": "collapse", - "enum": ["collapse", "expand"], + "enum": [ + "collapse", + "expand" + ], "markdownDescription": "%scss.format.braceStyle.desc%" }, "scss.format.preserveNewLines": { @@ -941,7 +947,10 @@ "type": "string", "scope": "resource", "default": "collapse", - "enum": ["collapse", "expand"], + "enum": [ + "collapse", + "expand" + ], "markdownDescription": "%less.format.braceStyle.desc%" }, "less.format.preserveNewLines": { @@ -985,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^7.0.0", + "vscode-languageclient": "^8.0.1", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.3" }, diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index 18c39540516..d2f0443e65b 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -10,8 +10,8 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^5.4.2", - "vscode-languageserver": "^7.0.0", + "vscode-css-languageservice": "^6.0.1", + "vscode-languageserver": "^8.0.1", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/css-language-features/server/src/cssServer.ts b/extensions/css-language-features/server/src/cssServer.ts index 40e05b13ab1..314aa8aebc0 100644 --- a/extensions/css-language-features/server/src/cssServer.ts +++ b/extensions/css-language-features/server/src/cssServer.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { - Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable, TextDocumentIdentifier, Range, FormattingOptions, TextEdit + Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType, Disposable, TextDocumentIdentifier, Range, FormattingOptions, TextEdit, Diagnostic } from 'vscode-languageserver'; import { URI } from 'vscode-uri'; 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 { runSafeAsync } from './utils/runner'; +import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; import { getDocumentContext } from './utils/documentContext'; import { fetchDataProviders } from './customData'; import { RequestService, getRequestService } from './requests'; @@ -56,6 +57,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let dataProvidersReady: Promise = Promise.resolve(); + let diagnosticsSupport: DiagnosticsSupport | undefined; + const languageServices: { [id: string]: LanguageService } = {}; const notReady = () => Promise.reject('Not Ready'); @@ -64,6 +67,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. connection.onInitialize((params: InitializeParams): InitializeResult => { + + const initializationOptions = params.initializationOptions as any || {}; + workspaceFolders = (params).workspaceFolders; if (!Array.isArray(workspaceFolders)) { workspaceFolders = []; @@ -72,7 +78,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } } - requestService = getRequestService(params.initializationOptions?.handledSchemas || ['file'], connection, runtime); + requestService = getRequestService(initializationOptions?.handledSchemas || ['file'], connection, runtime); function getClientCapability(name: string, def: T) { const keys = name.split('.'); @@ -88,12 +94,20 @@ 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; + + formatterMaxNumberOfEdits = initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; languageServices.css = getCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); languageServices.scss = getSCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); languageServices.less = getLESSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); + const supportsDiagnosticPull = getClientCapability('textDocument.diagnostic', undefined); + if (supportsDiagnosticPull === undefined) { + diagnosticsSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); + } else { + diagnosticsSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); + } + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/', '-', ':'] } : undefined, @@ -110,8 +124,13 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, - documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, - documentFormattingProvider: params.initializationOptions?.provideFormatter === true, + diagnosticProvider: { + documentSelector: null, + interFileDependencies: false, + workspaceDiagnostics: false + }, + documentRangeFormattingProvider: initializationOptions?.provideFormatter === true, + documentFormattingProvider: initializationOptions?.provideFormatter === true, }; return { capabilities }; }); @@ -135,7 +154,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let promise = documentSettings[textDocument.uri]; if (!promise) { const configRequestParam = { items: [{ scopeUri: textDocument.uri, section: textDocument.languageId }] }; - promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => s[0]); + promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => s[0] as LanguageSettings | undefined); documentSettings[textDocument.uri] = promise; } return promise; @@ -145,62 +164,25 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration(change => { - updateConfiguration(change.settings); + updateConfiguration(change.settings as any); }); - function updateConfiguration(settings: Settings) { + function updateConfiguration(settings: any) { for (const languageId in languageServices) { - languageServices[languageId].configure((settings as any)[languageId]); + languageServices[languageId].configure(settings[languageId]); } // reset all document settings documentSettings = {}; - // Revalidate any open text documents - documents.all().forEach(triggerValidation); + diagnosticsSupport?.requestRefresh(); } - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 500; - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent(change => { - triggerValidation(change.document); - }); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }); - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); - } - - function validateTextDocument(textDocument: TextDocument): void { + async function validateTextDocument(textDocument: TextDocument): Promise { const settingsPromise = getDocumentSettings(textDocument); - Promise.all([settingsPromise, dataProvidersReady]).then(async ([settings]) => { - const stylesheet = stylesheets.get(textDocument); - const diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings); - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }, e => { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - }); - } + const [settings] = await Promise.all([settingsPromise, dataProvidersReady]); + const stylesheet = stylesheets.get(textDocument); + return getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings); + } function updateDataProviders(dataPaths: string[]) { dataProvidersReady = fetchDataProviders(dataPaths, requestService).then(customDataProviders => { diff --git a/extensions/css-language-features/server/src/utils/validation.ts b/extensions/css-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..edd5f5618c7 --- /dev/null +++ b/extensions/css-language-features/server/src/utils/validation.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-css-languageservice'; +import { formatError, runSafeAsync } from './runner'; +import { RuntimeEnvironment } from '../cssServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticsSupport = { + dispose(): void; + requestRefresh(): void; +}; + +export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + return { + requestRefresh: () => { + documents.all().forEach(triggerValidation); + }, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} + +export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics + }; + } + + const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { + return runSafeAsync(runtime, async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + return newDocumentDiagnosticReport(await validate(document)); + } + return newDocumentDiagnosticReport([]); + + }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); + }); + + function requestRefresh(): void { + connection.languages.diagnostics.refresh(); + } + + return { + requestRefresh, + dispose: () => { + registration.dispose(); + } + }; + +} diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index b7a44bc52c4..8749dcdebad 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -12,50 +12,50 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -vscode-css-languageservice@^5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.4.2.tgz#69ea74c000bd653dfc8e458a1720d28b9ffa5cfb" - integrity sha512-DT7+7vfdT2HDNjDoXWtYJ0lVDdeDEdbMNdK4PKqUl2MS8g7PWt7J5G9B6k9lYox8nOfhCEjLnoNC3UKHHCR1lg== +vscode-css-languageservice@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.0.1.tgz#ccf94944e094dcc5833d1b4ac276994b698e9283" + integrity sha512-81n/eeYuJwQdvpoy6IK1258PtPbO720fl13FcJ5YQECPyHMFkmld1qKHwPJkyLbLPfboqJPM53ys4xW8v+iBVw== dependencies: vscode-languageserver-textdocument "^1.0.4" - vscode-languageserver-types "^3.16.0" - vscode-nls "^5.0.0" + vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" + vscode-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" 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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1, vscode-languageserver-types@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-languageserver@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" - integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== +vscode-languageserver@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.1.tgz#56bd7a01f5c88af075a77f1d220edcb30fc4bdc7" + integrity sha512-sn7SjBwWm3OlmLtgg7jbM0wBULppyL60rj8K5HF0ny/MzN+GzPBX1kCvYdybhl7UW63V5V5tRVnyB8iwC73lSQ== dependencies: - vscode-languageserver-protocol "3.16.0" + vscode-languageserver-protocol "3.17.1" -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== vscode-uri@^3.0.3: version "3.0.3" diff --git a/extensions/css-language-features/yarn.lock b/extensions/css-language-features/yarn.lock index 60ba7c2dcc3..76af92973f2 100644 --- a/extensions/css-language-features/yarn.lock +++ b/extensions/css-language-features/yarn.lock @@ -39,39 +39,39 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -semver@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageclient@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" - integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== +vscode-languageclient@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-8.0.1.tgz#bf5535c4463a78daeaca0bcb4f5868aec86bb301" + integrity sha512-9XoE+HJfaWvu7Y75H3VmLo5WLCtsbxEgEhrLPqwt7eyoR49lUIyyrjb98Yfa50JCMqF2cePJAEVI6oe2o1sIhw== dependencies: minimatch "^3.0.4" - semver "^7.3.4" - vscode-languageserver-protocol "3.16.0" + semver "^7.3.5" + vscode-languageserver-protocol "3.17.1" -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" + vscode-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" -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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== vscode-nls@^5.0.0: version "5.0.0" diff --git a/extensions/git/package.json b/extensions/git/package.json index c3c93123829..b5ad659a010 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -11,6 +11,7 @@ "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "enabledApiProposals": [ "diffCommand", + "contribMergeEditorToolbar", "contribViewsWelcome", "scmActionButton", "scmSelectedProvider", @@ -22,7 +23,8 @@ ], "activationEvents": [ "*", - "onFileSystem:git" + "onFileSystem:git", + "onFileSystem:git-show" ], "extensionDependencies": [ "vscode.git-base" @@ -564,6 +566,11 @@ "command": "git.api.getRemoteSources", "title": "%command.api.getRemoteSources%", "category": "Git API" + }, + { + "command": "git.acceptMerge", + "title": "%command.git.acceptMerge%", + "category": "Git" } ], "keybindings": [ @@ -1490,6 +1497,12 @@ "when": "isInDiffRightEditor && !isInEmbeddedDiffEditor && config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && isInDiffEditor && resourceScheme =~ /^git$|^file$/" } ], + "merge/toolbar": [ + { + "command": "git.acceptMerge", + "when": "isMergeEditor" + } + ], "scm/change/title": [ { "command": "git.stageChange", @@ -1852,7 +1865,8 @@ "git.branchPrefix": { "type": "string", "description": "%config.branchPrefix%", - "default": "" + "default": "", + "scope": "resource" }, "git.branchProtection": { "type": "array", @@ -1860,7 +1874,8 @@ "items": { "type": "string" }, - "default": [] + "default": [], + "scope": "resource" }, "git.branchProtectionPrompt": { "type": "string", @@ -1875,7 +1890,8 @@ "%config.branchProtectionPrompt.alwaysCommitToNewBranch%", "%config.branchProtectionPrompt.alwaysPrompt%" ], - "default": "alwaysPrompt" + "default": "alwaysPrompt", + "scope": "resource" }, "git.branchValidationRegex": { "type": "string", @@ -1890,7 +1906,8 @@ "git.branchRandomName.enable": { "type": "boolean", "description": "%config.branchRandomNameEnable%", - "default": false + "default": false, + "scope": "resource" }, "git.branchRandomName.dictionary": { "type": "array", @@ -1901,7 +1918,8 @@ "default": [ "adjectives", "animals" - ] + ], + "scope": "resource" }, "git.confirmSync": { "type": "boolean", @@ -2427,6 +2445,12 @@ ], "markdownDescription": "%config.logLevel%", "scope": "window" + }, + "git.experimental.mergeEditor": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.experimental.mergeEditor%", + "scope": "window" } } }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index ec691130508..e1a8c6a9faf 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -100,6 +100,7 @@ "command.api.getRepositories": "Get Repositories", "command.api.getRepositoryState": "Get Repository State", "command.api.getRemoteSources": "Get Remote Sources", + "command.git.acceptMerge": "Accept Merge", "config.enabled": "Whether git is enabled.", "config.path": "Path and filename of the git executable, e.g. `C:\\Program Files\\Git\\bin\\git.exe` (Windows). This can also be an array of string values containing multiple paths to look up.", "config.autoRepositoryDetection": "Configures when repositories should be automatically detected.", @@ -235,6 +236,7 @@ "config.logLevel.error": "Log only error, and critical information", "config.logLevel.critical": "Log only critical information", "config.logLevel.off": "Log nothing", + "config.experimental.mergeEditor": "Open the _experimental_ merge editor for files that are currently under conflict.", "submenu.explorer": "Git", "submenu.commit": "Commit", "submenu.commit.amend": "Amend", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index a3199cffc3a..e8cd512b648 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -6,7 +6,7 @@ import * as os from 'os'; import * as path from 'path'; import * as picomatch from 'picomatch'; -import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity } from 'vscode'; +import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; @@ -15,7 +15,7 @@ import { Git, Stash } from './git'; import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; -import { fromGitUri, toGitUri, isGitUri } from './uri'; +import { fromGitUri, toGitUri, isGitUri, toMergeUris } from './uri'; import { grep, isDescendant, pathEquals, relativePath } from './util'; import { LogLevel, OutputChannelLogger } from './log'; import { GitTimelineItem } from './timelineProvider'; @@ -405,6 +405,51 @@ export class CommandCenter { } } + @command('_git.openMergeEditor') + async openMergeEditor(uri: unknown) { + if (!(uri instanceof Uri)) { + return; + } + const repo = this.model.getRepository(uri); + if (!repo) { + return; + } + + + type InputData = { uri: Uri; detail?: string; description?: string }; + const mergeUris = toMergeUris(uri); + let input1: InputData = { uri: mergeUris.ours }; + let input2: InputData = { uri: mergeUris.theirs }; + + try { + const [head, mergeHead] = await Promise.all([repo.getCommit('HEAD'), repo.getCommit('MERGE_HEAD')]); + // ours (current branch and commit) + input1.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', '); + input1.description = head.hash.substring(0, 7); + + // theirs + input2.detail = mergeHead.refNames.join(', '); + input2.description = mergeHead.hash.substring(0, 7); + + } catch (error) { + // not so bad, can continue with just uris + console.error('FAILED to read HEAD, MERGE_HEAD commits'); + console.error(error); + } + + const options = { + ancestor: mergeUris.base, + input1, + input2, + output: uri + }; + + await commands.executeCommand( + '_open.mergeEditor', + options + ); + } + async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise { if (!url || typeof url !== 'string') { url = await pickRemoteSource({ @@ -416,6 +461,7 @@ export class CommandCenter { if (!url) { /* __GDPR__ "clone" : { + "owner": "lszomoru", "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -441,6 +487,7 @@ export class CommandCenter { if (!uris || uris.length === 0) { /* __GDPR__ "clone" : { + "owner": "lszomoru", "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -499,6 +546,7 @@ export class CommandCenter { /* __GDPR__ "clone" : { + "owner": "lszomoru", "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true } } @@ -518,6 +566,7 @@ export class CommandCenter { if (/already exists and is not an empty directory/.test(err && err.stderr || '')) { /* __GDPR__ "clone" : { + "owner": "lszomoru", "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -527,6 +576,7 @@ export class CommandCenter { } else { /* __GDPR__ "clone" : { + "owner": "lszomoru", "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -1035,6 +1085,34 @@ export class CommandCenter { await this._stageChanges(textEditor, selectedChanges); } + @command('git.acceptMerge') + async acceptMerge(uri: Uri | unknown): Promise { + if (!(uri instanceof Uri)) { + return; + } + const repository = this.model.getRepository(uri); + if (!repository) { + console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't belong to any repository`); + return; + } + + const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); + if (!doc) { + console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't match a document`); + return; + } + + await doc.save(); + await repository.add([uri]); + + // TODO@jrieken there isn't a `TabInputTextMerge` instance yet, till now the merge editor + // uses the `TabInputText` for the out-resource and we use that to identify and CLOSE the tab + const { activeTab } = window.tabGroups.activeTabGroup; + if (activeTab && activeTab?.input instanceof TabInputText && activeTab.input.uri.toString() === uri.toString()) { + await window.tabGroups.close(activeTab, true); + } + } + private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise { const modifiedDocument = textEditor.document; const modifiedUri = modifiedDocument.uri; @@ -2920,6 +2998,7 @@ export class CommandCenter { /* __GDPR__ "git.command" : { + "owner": "lszomoru", "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 8fdab7f659b..a511db761a6 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -354,7 +354,7 @@ function sanitizePath(path: string): string { return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); } -const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B'; +const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; export interface ICloneOptions { readonly parentPath: string; @@ -660,6 +660,7 @@ export interface Commit { authorName?: string; authorEmail?: string; commitDate?: Date; + refNames: string[]; } export class GitStatusParser { @@ -790,7 +791,7 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } -const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; +const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; export function parseGitCommits(data: string): Commit[] { let commits: Commit[] = []; @@ -801,6 +802,7 @@ export function parseGitCommits(data: string): Commit[] { let authorDate; let commitDate; let parents; + let refNames; let message; let match; @@ -810,7 +812,7 @@ export function parseGitCommits(data: string): Commit[] { break; } - [, ref, authorName, authorEmail, authorDate, commitDate, parents, message] = match; + [, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message] = match; if (message[message.length - 1] === '\n') { message = message.substr(0, message.length - 1); @@ -825,6 +827,7 @@ export function parseGitCommits(data: string): Commit[] { authorName: ` ${authorName}`.substr(1), authorEmail: ` ${authorEmail}`.substr(1), commitDate: new Date(Number(commitDate) * 1000), + refNames: refNames.split(',').map(s => s.trim()) }); } while (true); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 0327aae4868..ee60d2cb1b1 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -192,7 +192,9 @@ export async function _activate(context: ExtensionContext): Promise(command.command, ...(command.arguments || [])); } + + clone() { + return new Resource(this._commandResolver, this._resourceGroupType, this._resourceUri, this._type, this._useIcons, this._renameResourceUri); + } } export const enum Operation { @@ -546,7 +550,7 @@ class DotGitWatcher implements IFileWatcher { // Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files. // Watchman creates a cookie file inside the git directory whenever a query is run (https://facebook.github.io/watchman/docs/cookies.html). - const filteredRootWatcher = filterEvent(rootWatcher.event, uri => !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path)); + const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path)); this.event = anyEvent(filteredRootWatcher, this.emitter.event); repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables); @@ -603,11 +607,20 @@ class ResourceCommandResolver { const title = this.getTitle(resource); if (!resource.leftUri) { - return { - command: 'vscode.open', - title: localize('open', "Open"), - arguments: [resource.rightUri, { override: resource.type === Status.BOTH_MODIFIED ? false : undefined }, title] - }; + const bothModified = resource.type === Status.BOTH_MODIFIED; + if (resource.rightUri && bothModified && workspace.getConfiguration('git').get('experimental.mergeEditor', false)) { + return { + command: '_git.openMergeEditor', + title: localize('open.merge', "Open Merge"), + arguments: [resource.rightUri] + }; + } else { + return { + command: 'vscode.open', + title: localize('open', "Open"), + arguments: [resource.rightUri, { override: bothModified ? false : undefined }, title] + }; + } } else { return { command: 'vscode.diff', @@ -912,6 +925,12 @@ export class Repository implements Disposable { onConfigListener(updateIndexGroupVisibility, this, this.disposables); updateIndexGroupVisibility(); + workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('git.experimental.mergeEditor')) { + this.mergeGroup.resourceStates = this.mergeGroup.resourceStates.map(r => r.clone()); + } + }, undefined, this.disposables); + filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root) || e.affectsConfiguration('git.untrackedChanges', root) @@ -1382,15 +1401,15 @@ export class Repository implements Disposable { } @throttle - async fetchAll(): Promise { - await this._fetch({ all: true }); + async fetchAll(cancellationToken?: CancellationToken): Promise { + await this._fetch({ all: true, cancellationToken }); } async fetch(options: FetchOptions): Promise { await this._fetch(options); } - private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean } = {}): Promise { + private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; cancellationToken?: CancellationToken } = {}): Promise { if (!options.prune) { const config = workspace.getConfiguration('git', Uri.file(this.root)); const prune = config.get('pruneOnFetch'); @@ -1435,7 +1454,7 @@ export class Repository implements Disposable { // When fetchOnPull is enabled, fetch all branches when pulling if (fetchOnPull) { - await this.repository.fetch({ all: true }); + await this.fetchAll(); } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { @@ -1506,7 +1525,7 @@ export class Repository implements Disposable { const fn = async (cancellationToken?: CancellationToken) => { // When fetchOnPull is enabled, fetch all branches when pulling if (fetchOnPull) { - await this.repository.fetch({ all: true, cancellationToken }); + await this.fetchAll(cancellationToken); } if (await this.checkIfMaybeRebased(this.HEAD?.name)) { @@ -1864,6 +1883,7 @@ export class Repository implements Disposable { if (didHitLimit) { /* __GDPR__ "statusLimit" : { + "owner": "lszomoru", "ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index c0d0d2003c9..3b225157c3b 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -205,6 +205,7 @@ john.doe@mail.com 1580811030 1580811031 8e5a374372b8393906c7e380dbb09349c5385554 +main,branch This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ @@ -215,6 +216,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main', 'branch'], }]); }); @@ -225,6 +227,7 @@ john.doe@mail.com 1580811030 1580811031 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 +main This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ @@ -235,6 +238,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main'], }]); }); @@ -245,6 +249,7 @@ john.doe@mail.com 1580811030 1580811031 +main This is a commit message.\x00`; assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ @@ -255,6 +260,7 @@ This is a commit message.\x00`; authorName: 'John Doe', authorEmail: 'john.doe@mail.com', commitDate: new Date(1580811031000), + refNames: ['main'], }]); }); }); diff --git a/extensions/git/src/uri.ts b/extensions/git/src/uri.ts index 94e6b5e38ae..5694c920d6b 100644 --- a/extensions/git/src/uri.ts +++ b/extensions/git/src/uri.ts @@ -51,3 +51,14 @@ export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Ur query: JSON.stringify(params) }); } + +/** + * Assuming `uri` is being merged it creates uris for `base`, `ours`, and `theirs` + */ +export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } { + return { + base: toGitUri(uri, ':1'), + ours: toGitUri(uri, ':2'), + theirs: toGitUri(uri, ':3'), + }; +} diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index c366ff3bff6..6816d9d992e 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -496,6 +496,7 @@ export class GitHubServer implements IGitHubServer { /* __GDPR__ "session" : { + "owner": "TylerLeonhardt", "isEdu": { "classification": "NonIdentifiableDemographicInfo", "purpose": "FeatureInsight" } } */ @@ -530,6 +531,7 @@ export class GitHubServer implements IGitHubServer { /* __GDPR__ "ghe-session" : { + "owner": "TylerLeonhardt", "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ @@ -601,6 +603,7 @@ export class GitHubEnterpriseServer implements IGitHubServer { /* __GDPR__ "ghe-session" : { + "owner": "TylerLeonhardt", "version": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index 37f138caffb..5569d4fb480 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -102,13 +102,14 @@ async function handlePushError(repository: Repository, remote: Remote, refspec: let title = `Update ${remoteName}`; const head = repository.state.HEAD?.name; + let body: string | undefined; + if (head) { const commit = await repository.getCommit(head); - title = commit.message.replace(/\n.*$/m, ''); + title = commit.message.split('\n')[0]; + body = commit.message.slice(title.length + 1).trim(); } - let body: string | undefined; - const templates = await findPullRequestTemplates(repository.rootUri); if (templates.length > 0) { templates.sort((a, b) => a.path.localeCompare(b.path)); diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts index b69cca75854..ab23520fe79 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, ExtensionContext, Uri } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; +import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../htmlClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -15,8 +15,10 @@ declare const TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string }; }; +let client: BaseLanguageClient | undefined; + // this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/htmlServerMain.js'); try { const worker = new Worker(serverMain.toString()); @@ -31,9 +33,17 @@ export function activate(context: ExtensionContext) { } }; - startClient(context, newLanguageClient, { TextDecoder, timer }); + client = await startClient(context, newLanguageClient, { TextDecoder, timer }); } catch (e) { console.log(e); } } + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } +} + diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 7baceece6fb..6c44ceb72a8 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -13,7 +13,7 @@ import { } from 'vscode'; import { LanguageClientOptions, RequestType, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, CommonLanguageClient + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, BaseLanguageClient } from 'vscode-languageclient'; import { FileSystemProvider, serveFileSystemRequests } from './requests'; import { getCustomDataSource } from './customData'; @@ -72,7 +72,7 @@ export interface TelemetryReporter { }): void; } -export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; +export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; export interface Runtime { TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } }; @@ -83,18 +83,17 @@ export interface Runtime { }; } -export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { +export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - let toDispose = context.subscriptions; + const toDispose = context.subscriptions; - - let documentSelector = ['html', 'handlebars']; - let embeddedLanguages = { css: true, javascript: true }; + const documentSelector = ['html', 'handlebars']; + const embeddedLanguages = { css: true, javascript: true }; let rangeFormatting: Disposable | undefined = undefined; // Options to control the language client - let clientOptions: LanguageClientOptions = { + const clientOptions: LanguageClientOptions = { documentSelector, synchronize: { configurationSection: ['html', 'css', 'javascript'], // the settings to synchronize @@ -135,67 +134,65 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions); client.registerProposedFeatures(); - let disposable = client.start(); - toDispose.push(disposable); - client.onReady().then(() => { + await client.start(); - toDispose.push(serveFileSystemRequests(client, runtime)); + toDispose.push(serveFileSystemRequests(client, runtime)); - const customDataSource = getCustomDataSource(runtime, context.subscriptions); + const customDataSource = getCustomDataSource(runtime, context.subscriptions); + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + customDataSource.onDidChange(() => { client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); - customDataSource.onDidChange(() => { - client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); - }); - client.onRequest(CustomDataContent.type, customDataSource.getContent); - - - const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise => { - let param: AutoInsertParams = { - kind, - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), - position: client.code2ProtocolConverter.asPosition(position) - }; - return client.sendRequest(AutoInsertRequest.type, param); - }; - let disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime); - toDispose.push(disposable); - - disposable = client.onTelemetry(e => { - runtime.telemetry?.sendTelemetryEvent(e.key, e.data); - }); - toDispose.push(disposable); - - // manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652. - updateFormatterRegistration(); - toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); - toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration())); - - client.sendRequest(SemanticTokenLegendRequest.type).then(legend => { - if (legend) { - const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = { - provideDocumentSemanticTokens(doc) { - const params: SemanticTokenParams = { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), - }; - return client.sendRequest(SemanticTokenRequest.type, params).then(data => { - return data && new SemanticTokens(new Uint32Array(data)); - }); - }, - provideDocumentRangeSemanticTokens(doc, range) { - const params: SemanticTokenParams = { - textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), - ranges: [client.code2ProtocolConverter.asRange(range)] - }; - return client.sendRequest(SemanticTokenRequest.type, params).then(data => { - return data && new SemanticTokens(new Uint32Array(data)); - }); - } - }; - toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers))); - } - }); }); + client.onRequest(CustomDataContent.type, customDataSource.getContent); + + + const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise => { + const param: AutoInsertParams = { + kind, + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document), + position: client.code2ProtocolConverter.asPosition(position) + }; + return client.sendRequest(AutoInsertRequest.type, param); + }; + const disposable = activateAutoInsertion(insertRequestor, { html: true, handlebars: true }, runtime); + toDispose.push(disposable); + + const disposable2 = client.onTelemetry(e => { + runtime.telemetry?.sendTelemetryEvent(e.key, e.data); + }); + toDispose.push(disposable2); + + // manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652. + updateFormatterRegistration(); + toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); + toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration())); + + client.sendRequest(SemanticTokenLegendRequest.type).then(legend => { + if (legend) { + const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = { + provideDocumentSemanticTokens(doc) { + const params: SemanticTokenParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), + }; + return client.sendRequest(SemanticTokenRequest.type, params).then(data => { + return data && new SemanticTokens(new Uint32Array(data)); + }); + }, + provideDocumentRangeSemanticTokens(doc, range) { + const params: SemanticTokenParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), + ranges: [client.code2ProtocolConverter.asRange(range)] + }; + return client.sendRequest(SemanticTokenRequest.type, params).then(data => { + return data && new SemanticTokens(new Uint32Array(data)); + }); + } + }; + toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers))); + } + }); + function updateFormatterRegistration() { const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable); @@ -219,7 +216,7 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( client.protocol2CodeConverter.asTextEdits, (error) => { - client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); + client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []); return Promise.resolve([]); } ); @@ -300,4 +297,6 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua } } + return client; + } diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index 4c7d24e397c..f460d0c1524 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -6,16 +6,17 @@ import { getNodeFileFS } from './nodeFs'; import { Disposable, ExtensionContext } from 'vscode'; import { startClient, LanguageClientConstructor } from '../htmlClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; import { TextDecoder } from 'util'; import * as fs from 'fs'; import TelemetryReporter from '@vscode/extension-telemetry'; let telemetry: TelemetryReporter | undefined; +let client: BaseLanguageClient | undefined; // this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { let clientPackageJSON = getPackageInfo(context); telemetry = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey); @@ -44,7 +45,14 @@ export function activate(context: ExtensionContext) { } }; - startClient(context, newLanguageClient, { fileFs: getNodeFileFS(), TextDecoder, telemetry, timer }); + client = await startClient(context, newLanguageClient, { fileFs: getNodeFileFS(), TextDecoder, telemetry, timer }); +} + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } } interface IPackageInfo { diff --git a/extensions/html-language-features/client/src/requests.ts b/extensions/html-language-features/client/src/requests.ts index ba124e28cd7..8106f044228 100644 --- a/extensions/html-language-features/client/src/requests.ts +++ b/extensions/html-language-features/client/src/requests.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Uri, workspace, Disposable } from 'vscode'; -import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; +import { RequestType, BaseLanguageClient } from 'vscode-languageclient'; import { Runtime } from './htmlClient'; export namespace FsStatRequest { @@ -15,7 +15,7 @@ export namespace FsReadDirRequest { export const type: RequestType = new RequestType('fs/readDir'); } -export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime): Disposable { +export function serveFileSystemRequests(client: BaseLanguageClient, runtime: Runtime): Disposable { const disposables = []; disposables.push(client.onRequest(FsReadDirRequest.type, (uriString: string) => { const uri = Uri.parse(uriString); diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index e7244d251c0..2f2dc4d1067 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -261,9 +261,9 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "0.4.10", - "vscode-languageclient": "^7.0.0", - "vscode-nls": "^5.0.0", + "@vscode/extension-telemetry": "0.5.1", + "vscode-languageclient": "^8.0.1", + "vscode-nls": "^5.0.1", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/html-language-features/server/build/javaScriptLibraryLoader.js b/extensions/html-language-features/server/build/javaScriptLibraryLoader.js index 85792138ba4..51e74618042 100644 --- a/extensions/html-language-features/server/build/javaScriptLibraryLoader.js +++ b/extensions/html-language-features/server/build/javaScriptLibraryLoader.js @@ -31,7 +31,7 @@ module.exports = function () { queue.push(name); }; - enqueue('es6'); + enqueue('es2020.full'); var result = []; while (queue.length > 0) { diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 423a395ba96..affcf98e566 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,11 +9,11 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^5.4.2", - "vscode-html-languageservice": "^4.2.5", - "vscode-languageserver": "^7.0.0", - "vscode-languageserver-textdocument": "^1.0.3", - "vscode-nls": "^5.0.0", + "vscode-css-languageservice": "^6.0.1", + "vscode-html-languageservice": "^5.0.0", + "vscode-languageserver": "^8.0.1", + "vscode-languageserver-textdocument": "^1.0.4", + "vscode-nls": "^5.0.1", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index 6713556ae38..99e3cb75bcd 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -11,7 +11,7 @@ import { } from 'vscode-languageserver'; import { getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, - Range, DocumentLink, SymbolInformation, TextDocumentIdentifier + Range, DocumentLink, SymbolInformation, TextDocumentIdentifier, isCompletionItemData } from './modes/languageModes'; import { format } from './modes/formatting'; @@ -19,6 +19,7 @@ import { pushAll } from './utils/arrays'; import { getDocumentContext } from './utils/documentContext'; import { URI } from 'vscode-uri'; import { formatError, runSafe } from './utils/runner'; +import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; import { getFoldingRanges } from './modes/htmlFolding'; import { fetchHTMLDataProviders } from './customData'; @@ -92,6 +93,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let languageModes: LanguageModes; + let diagnosticsSupport: DiagnosticsSupport | undefined; + let clientSnippetSupport = false; let dynamicFormatterRegistration = false; let scopedSettingsSupport = false; @@ -129,7 +132,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities connection.onInitialize((params: InitializeParams): InitializeResult => { - const initializationOptions = params.initializationOptions; + const initializationOptions = params.initializationOptions as any || {}; workspaceFolders = (params).workspaceFolders; if (!Array.isArray(workspaceFolders)) { @@ -179,14 +182,22 @@ 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; + formatterMaxNumberOfEdits = initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; + + const supportsDiagnosticPull = getClientCapability('textDocument.diagnostic', undefined); + if (supportsDiagnosticPull === undefined) { + diagnosticsSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); + } else { + diagnosticsSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); + } + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, hoverProvider: true, documentHighlightProvider: true, - documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, - documentFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentRangeFormattingProvider: initializationOptions?.provideFormatter === true, + documentFormattingProvider: initializationOptions?.provideFormatter === true, documentLinkProvider: { resolveProvider: false }, documentSymbolProvider: true, definitionProvider: true, @@ -196,7 +207,12 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) foldingRangeProvider: true, selectionRangeProvider: true, renameProvider: true, - linkedEditingRangeProvider: true + linkedEditingRangeProvider: true, + diagnosticProvider: { + documentSelector: null, + interFileDependencies: false, + workspaceDiagnostics: false + } }; return { capabilities }; }); @@ -217,7 +233,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } } workspaceFolders = updatedFolders.concat(toAdd); - documents.all().forEach(triggerValidation); + diagnosticsSupport?.requestRefresh(); }); } }); @@ -226,9 +242,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { - globalSettings = change.settings; + globalSettings = change.settings as Settings; documentSettings = {}; // reset all document settings - documents.all().forEach(triggerValidation); + diagnosticsSupport?.requestRefresh(); // dynamically enable & disable the formatter if (dynamicFormatterRegistration) { @@ -248,37 +264,6 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } }); - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 500; - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent(change => { - triggerValidation(change.document); - }); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }); - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); - } - function isValidationEnabled(languageId: string, settings: Settings = globalSettings) { const validationSettings = settings && settings.html && settings.html.validate; if (validationSettings) { @@ -287,7 +272,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) return true; } - async function validateTextDocument(textDocument: TextDocument) { + async function validateTextDocument(textDocument: TextDocument): Promise { try { const version = textDocument.version; const diagnostics: Diagnostic[] = []; @@ -301,12 +286,13 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) pushAll(diagnostics, await mode.doValidation(latestTextDocument, settings)); } } - connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics }); + return diagnostics; } } } catch (e) { connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); } + return []; } connection.onCompletion(async (textDocumentPosition, token) => { @@ -321,15 +307,6 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } const doComplete = mode.doComplete; - if (mode.getId() !== 'html') { - /* __GDPR__ - "html.embbedded.complete" : { - "languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } }); - } - const settings = await getDocumentSettings(document, () => doComplete.length > 2); const documentContext = getDocumentContext(document.uri, workspaceFolders); return doComplete(document, textDocumentPosition.position, documentContext, settings); @@ -340,7 +317,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) connection.onCompletionResolve((item, token) => { return runSafe(runtime, async () => { const data = item.data; - if (data && data.languageId && data.uri) { + if (isCompletionItemData(data)) { const mode = languageModes.getMode(data.languageId); const document = documents.get(data.uri); if (mode && mode.doResolve && document) { diff --git a/extensions/html-language-features/server/src/modes/cssMode.ts b/extensions/html-language-features/server/src/modes/cssMode.ts index 9757d99cd88..6bc02acb510 100644 --- a/extensions/html-language-features/server/src/modes/cssMode.ts +++ b/extensions/html-language-features/server/src/modes/cssMode.ts @@ -5,7 +5,7 @@ import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; -import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes'; +import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext, Diagnostic } from './languageModes'; import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { @@ -18,7 +18,7 @@ export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegio }, async doValidation(document: TextDocument, settings = workspace.settings) { let embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css); + return (cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css) as Diagnostic[]); }, async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) { let embedded = embeddedCSSDocuments.get(document); diff --git a/extensions/html-language-features/server/src/modes/javascriptLibs.ts b/extensions/html-language-features/server/src/modes/javascriptLibs.ts index bdca89be362..7abf94edf22 100644 --- a/extensions/html-language-features/server/src/modes/javascriptLibs.ts +++ b/extensions/html-language-features/server/src/modes/javascriptLibs.ts @@ -24,7 +24,7 @@ export function loadLibrary(name: string) { try { content = readFileSync(libPath).toString(); } catch (e) { - console.log(`Unable to load library ${name} at ${libPath}: ${e.message}`); + console.log(`Unable to load library ${name} at ${libPath}`); content = ''; } contents[name] = content; diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index bbcba5e0f86..a119a9248ae 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -8,7 +8,7 @@ import { SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange, - LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext + LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext, CompletionItemData, isCompletionItemData } from './languageModes'; import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings'; import { HTMLDocumentRegions } from './embeddedSupport'; @@ -19,7 +19,7 @@ import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticT const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; function getLanguageServiceHost(scriptKind: ts.ScriptKind) { - const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false }; + const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es2020.full.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false }; let currentTextDocument = TextDocument.create('init', 'javascript', 1, ''); const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => { @@ -52,7 +52,7 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) { }; }, getCurrentDirectory: () => '', - getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es6', + getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es2020.full', readFile: (path: string, _encoding?: string | undefined): string | undefined => { if (path === currentTextDocument.uri) { return currentTextDocument.getText(); @@ -66,6 +66,15 @@ function getLanguageServiceHost(scriptKind: ts.ScriptKind) { } else { return !!libs.loadLibrary(path); } + }, + directoryExists: (path: string): boolean => { + // typescript tries to first find libraries in node_modules/@types and node_modules/@typescript + // there's no node_modules in our setup + if (path.startsWith('node_modules')) { + return false; + } + return true; + } }; return ts.createLanguageService(host); @@ -122,6 +131,11 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { + const data: CompletionItemData = { // data used for resolving item details (see 'doResolve') + languageId, + uri: document.uri, + offset: offset + }; return { uri: document.uri, position: position, @@ -129,23 +143,21 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined); - if (details) { - item.detail = ts.displayPartsToString(details.displayParts); - item.documentation = ts.displayPartsToString(details.documentation); - delete item.data; + if (isCompletionItemData(item.data)) { + const jsDocument = jsDocuments.get(document); + const jsLanguageService = await host.getLanguageService(jsDocument); + let details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined); + if (details) { + item.detail = ts.displayPartsToString(details.displayParts); + item.documentation = ts.displayPartsToString(details.documentation); + delete item.data; + } } return item; }, diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index 3a8b9b797cc..59cc742b2b8 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -54,6 +54,16 @@ export interface SemanticTokenData { modifierSet: number; } +export type CompletionItemData = { + languageId: string; + uri: string; + offset: number; +}; + +export function isCompletionItemData(value: any): value is CompletionItemData { + return value && typeof value.languageId === 'string' && typeof value.uri === 'string' && typeof value.offset === 'number'; +} + export interface LanguageMode { getId(): string; getSelectionRange?: (document: TextDocument, position: Position) => Promise; diff --git a/extensions/html-language-features/server/src/utils/validation.ts b/extensions/html-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..adb13086391 --- /dev/null +++ b/extensions/html-language-features/server/src/utils/validation.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-html-languageservice'; +import { formatError, runSafe } from './runner'; +import { RuntimeEnvironment } from '../htmlServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticsSupport = { + dispose(): void; + requestRefresh(): void; +}; + +export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + return { + requestRefresh: () => { + documents.all().forEach(triggerValidation); + }, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} + +export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics + }; + } + + const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { + return runSafe(runtime, async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + return newDocumentDiagnosticReport(await validate(document)); + } + return newDocumentDiagnosticReport([]); + + }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); + }); + + function requestRefresh(): void { + connection.languages.diagnostics.refresh(); + } + + return { + requestRefresh, + dispose: () => { + registration.dispose(); + } + }; + +} diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index a5bfebda600..f086cbf6898 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -12,65 +12,60 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -vscode-css-languageservice@^5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-5.4.2.tgz#69ea74c000bd653dfc8e458a1720d28b9ffa5cfb" - integrity sha512-DT7+7vfdT2HDNjDoXWtYJ0lVDdeDEdbMNdK4PKqUl2MS8g7PWt7J5G9B6k9lYox8nOfhCEjLnoNC3UKHHCR1lg== +vscode-css-languageservice@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.0.1.tgz#ccf94944e094dcc5833d1b4ac276994b698e9283" + integrity sha512-81n/eeYuJwQdvpoy6IK1258PtPbO720fl13FcJ5YQECPyHMFkmld1qKHwPJkyLbLPfboqJPM53ys4xW8v+iBVw== dependencies: vscode-languageserver-textdocument "^1.0.4" - vscode-languageserver-types "^3.16.0" - vscode-nls "^5.0.0" + vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-html-languageservice@^4.2.5: - version "4.2.5" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-4.2.5.tgz#c0cc8ff3d824d16388bbac187e1828749eccf006" - integrity sha512-dbr10KHabB9EaK8lI0XZW7SqOsTfrNyT3Nuj0GoPi4LjGKUmMiLtsqzfedIzRTzqY+w0FiLdh0/kQrnQ0tLxrw== +vscode-html-languageservice@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-5.0.0.tgz#c68613f836d7fcff125183d78e6f1f0ff326fa55" + integrity sha512-KJG13z54aLszskp3ETf8b1EKDypr2Sf5RUsfR6OXmKqEl2ZUfyIxsWz4gbJWjPzoJZx/bGH0ZXVwxJ1rg8OKRQ== dependencies: vscode-languageserver-textdocument "^1.0.4" - vscode-languageserver-types "^3.16.0" - vscode-nls "^5.0.0" + vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" - -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-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" 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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1, vscode-languageserver-types@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-languageserver@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" - integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== +vscode-languageserver@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.1.tgz#56bd7a01f5c88af075a77f1d220edcb30fc4bdc7" + integrity sha512-sn7SjBwWm3OlmLtgg7jbM0wBULppyL60rj8K5HF0ny/MzN+GzPBX1kCvYdybhl7UW63V5V5tRVnyB8iwC73lSQ== dependencies: - vscode-languageserver-protocol "3.16.0" + vscode-languageserver-protocol "3.17.1" -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== vscode-uri@^3.0.3: version "3.0.3" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index d77421b701c..5157b15d877 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@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/extension-telemetry@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.5.1.tgz#20150976629663b3d33799a4ad25944a1535f7db" + integrity sha512-cvFq8drxdLRF8KN72WcV4lTEa9GqDiRwy9EbnYuoSCD9Jdk8zHFF49MmACC1qs4R9Ko/C1uMOmeLJmVi8EA0rQ== balanced-match@^1.0.0: version "1.0.0" @@ -44,44 +44,44 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -semver@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageclient@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" - integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== +vscode-languageclient@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-8.0.1.tgz#bf5535c4463a78daeaca0bcb4f5868aec86bb301" + integrity sha512-9XoE+HJfaWvu7Y75H3VmLo5WLCtsbxEgEhrLPqwt7eyoR49lUIyyrjb98Yfa50JCMqF2cePJAEVI6oe2o1sIhw== dependencies: minimatch "^3.0.4" - semver "^7.3.4" - vscode-languageserver-protocol "3.16.0" + semver "^7.3.5" + vscode-languageserver-protocol "3.17.1" -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" + vscode-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" -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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== vscode-uri@^3.0.3: version "3.0.3" diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index c8feabc17af..1e251a5a19b 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -9,8 +9,7 @@ "vscode": "^1.57.0" }, "enabledApiProposals": [ - "notebookEditor", - "notebookEditorEdit" + "notebookWorkspaceEdit" ], "activationEvents": [ "*" diff --git a/extensions/ipynb/src/cellIdService.ts b/extensions/ipynb/src/cellIdService.ts index 082d25add97..ddda0a9fd5f 100644 --- a/extensions/ipynb/src/cellIdService.ts +++ b/extensions/ipynb/src/cellIdService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext, NotebookDocument, NotebookDocumentChangeEvent, workspace, WorkspaceEdit } from 'vscode'; +import { ExtensionContext, NotebookDocument, NotebookDocumentChangeEvent, NotebookEdit, workspace, WorkspaceEdit } from 'vscode'; import { v4 as uuid } from 'uuid'; import { getCellMetadata } from './serializers'; import { CellMetadata } from './common'; @@ -34,7 +34,7 @@ function onDidChangeNotebookCells(e: NotebookDocumentChangeEvent) { // Don't edit the metadata directly, always get a clone (prevents accidental singletons and directly editing the objects). const updatedMetadata: CellMetadata = { ...JSON.parse(JSON.stringify(cellMetadata || {})) }; updatedMetadata.id = id; - edit.replaceNotebookCellMetadata(cell.notebook.uri, cell.index, { ...(cell.metadata), custom: updatedMetadata }); + edit.set(cell.notebook.uri, [NotebookEdit.updateCellMetadata(cell.index, { ...(cell.metadata), custom: updatedMetadata })]); workspace.applyEdit(edit); }); }); diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts index c2c7044bd77..33bb1456af8 100644 --- a/extensions/ipynb/src/ipynbMain.ts +++ b/extensions/ipynb/src/ipynbMain.ts @@ -94,7 +94,7 @@ export function activate(context: vscode.ExtensionContext) { } const edit = new vscode.WorkspaceEdit(); - edit.replaceNotebookMetadata(resource, { + edit.set(resource, [vscode.NotebookEdit.updateNotebookMetadata({ ...document.metadata, custom: { ...(document.metadata.custom ?? {}), @@ -103,7 +103,7 @@ export function activate(context: vscode.ExtensionContext) { ...metadata }, } - }); + })]); return vscode.workspace.applyEdit(edit); }, }; diff --git a/extensions/ipynb/tsconfig.json b/extensions/ipynb/tsconfig.json index 178e86493b4..a8006017458 100644 --- a/extensions/ipynb/tsconfig.json +++ b/extensions/ipynb/tsconfig.json @@ -9,7 +9,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.notebookEditor.d.ts", - "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts" + "../../src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts" ] } diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index 9eb390b545c..e1fae6ffeb4 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionContext, Uri } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; +import { BaseLanguageClient, LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor, SchemaRequestService } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -14,8 +14,10 @@ declare const Worker: { declare function fetch(uri: string, options: any): any; +let client: BaseLanguageClient | undefined; + // this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export async function activate(context: ExtensionContext) { const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/jsonServerMain.js'); try { const worker = new Worker(serverMain.toString()); @@ -32,9 +34,16 @@ export function activate(context: ExtensionContext) { } }; - startClient(context, newLanguageClient, { schemaRequests }); + client = await startClient(context, newLanguageClient, { schemaRequests }); } catch (e) { console.log(e); } } + +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } +} diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 9bf63477ebe..60225af8cef 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -16,9 +16,10 @@ import { import { LanguageClientOptions, RequestType, NotificationType, DidChangeConfigurationNotification, HandleDiagnosticsSignature, ResponseError, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, CommonLanguageClient + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, ProvideHoverSignature, BaseLanguageClient } from 'vscode-languageclient'; + import { hash } from './utils/hash'; import { createLanguageStatusItem } from './languageStatus'; @@ -56,7 +57,7 @@ namespace ResultLimitReachedNotification { export const type: NotificationType = new NotificationType('json/resultLimitReached'); } -interface Settings { +type Settings = { json?: { schemas?: JSONSchemaSettings[]; format?: { enable?: boolean }; @@ -67,13 +68,13 @@ interface Settings { proxy?: string; proxyStrictSSL?: boolean; }; -} +}; -export interface JSONSchemaSettings { +export type JSONSchemaSettings = { fileMatch?: string[]; url?: string; schema?: any; -} +}; namespace SettingIds { export const enableFormatter = 'json.format.enable'; @@ -94,7 +95,7 @@ export interface TelemetryReporter { }): void; } -export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; +export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient; export interface Runtime { schemaRequests: SchemaRequestService; @@ -108,7 +109,7 @@ export interface SchemaRequestService { export const languageServerDescription = localize('jsonserver.name', 'JSON Language Server'); -export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { +export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { const toDispose = context.subscriptions; @@ -218,176 +219,177 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua const client = newLanguageClient('json', languageServerDescription, clientOptions); client.registerProposedFeatures(); - const disposable = client.start(); - toDispose.push(disposable); - client.onReady().then(() => { - isClientReady = true; + const schemaDocuments: { [uri: string]: boolean } = {}; - const schemaDocuments: { [uri: string]: boolean } = {}; - - // handle content request - client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { - const uri = Uri.parse(uriPath); - if (uri.scheme === 'untitled') { - return Promise.reject(new ResponseError(3, localize('untitled.schema', 'Unable to load {0}', uri.toString()))); - } - if (uri.scheme !== 'http' && uri.scheme !== 'https') { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(new ResponseError(2, error.toString())); - }); - } else if (schemaDownloadEnabled) { - if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { - /* __GDPR__ - "json.schema" : { - "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); - } - return runtime.schemaRequests.getContent(uriPath).catch(e => { - return Promise.reject(new ResponseError(4, e.toString())); - }); - } else { - return Promise.reject(new ResponseError(1, localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); - } - }); - - const handleContentChange = (uriString: string) => { - if (schemaDocuments[uriString]) { - client.sendNotification(SchemaContentChangeNotification.type, uriString); - return true; - } - return false; - }; - const handleActiveEditorChange = (activeEditor?: TextEditor) => { - if (!activeEditor) { - return; - } - - const activeDocUri = activeEditor.document.uri.toString(); - - if (activeDocUri && fileSchemaErrors.has(activeDocUri)) { - schemaResolutionErrorStatusBarItem.show(); - } else { - schemaResolutionErrorStatusBarItem.hide(); - } - }; - - toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); - toDispose.push(workspace.onDidCloseTextDocument(d => { - const uriString = d.uri.toString(); - if (handleContentChange(uriString)) { - delete schemaDocuments[uriString]; - } - fileSchemaErrors.delete(uriString); - })); - toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); - - const handleRetryResolveSchemaCommand = () => { - if (window.activeTextEditor) { - schemaResolutionErrorStatusBarItem.text = '$(watch)'; - const activeDocUri = window.activeTextEditor.document.uri.toString(); - client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => { - const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); - if (schemaErrorIndex !== -1) { - // Show schema resolution errors in status bar only; ref: #51032 - const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; - fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message); - } else { - schemaResolutionErrorStatusBarItem.hide(); - } - schemaResolutionErrorStatusBarItem.text = '$(alert)'; - }); - } - }; - - toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand)); - - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); - - 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(); - toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); - - updateSchemaDownloadSetting(); - - toDispose.push(workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(SettingIds.enableFormatter)) { - updateFormatterRegistration(); - } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { - updateSchemaDownloadSetting(); - } - })); - - client.onNotification(ResultLimitReachedNotification.type, async message => { - const shouldPrompt = context.globalState.get(StorageIds.maxItemsExceededInformation) !== false; - if (shouldPrompt) { - 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); - if (pick === neverAgain) { - await context.globalState.update(StorageIds.maxItemsExceededInformation, false); - } else if (pick === openSettings) { - await commands.executeCommand('workbench.action.openSettings', SettingIds.maxItemsComputed); - } - } - }); - - toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri))); - - function updateFormatterRegistration() { - const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter); - if (!formatEnabled && rangeFormatting) { - rangeFormatting.dispose(); - rangeFormatting = undefined; - } else if (formatEnabled && !rangeFormatting) { - rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { - 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) - }; - - return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( - client.protocol2CodeConverter.asTextEdits, - (error) => { - client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); - return Promise.resolve([]); - } - ); - } - }); - } + // handle content request + client.onRequest(VSCodeContentRequest.type, (uriPath: string) => { + const uri = Uri.parse(uriPath); + if (uri.scheme === 'untitled') { + return Promise.reject(new ResponseError(3, localize('untitled.schema', 'Unable to load {0}', uri.toString()))); } - - function updateSchemaDownloadSetting() { - schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false; - if (schemaDownloadEnabled) { - schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionErrorMessage', 'Unable to resolve schema. Click to retry.'); - schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema'; - handleRetryResolveSchemaCommand(); - } else { - schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionDisabledMessage', 'Downloading schemas is disabled. Click to configure.'); - schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' }; + if (uri.scheme !== 'http' && uri.scheme !== 'https') { + return workspace.openTextDocument(uri).then(doc => { + schemaDocuments[uri.toString()] = true; + return doc.getText(); + }, error => { + return Promise.reject(new ResponseError(2, error.toString())); + }); + } else if (schemaDownloadEnabled) { + if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { + /* __GDPR__ + "json.schema" : { + "owner": "aeschli", + "comment": "Measure the use of the Azure resource manager schemas", + "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." } + } + */ + runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriPath }); } + return runtime.schemaRequests.getContent(uriPath).catch(e => { + return Promise.reject(new ResponseError(4, e.toString())); + }); + } else { + return Promise.reject(new ResponseError(1, localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload))); } - }); + + await client.start(); + + isClientReady = true; + + const handleContentChange = (uriString: string) => { + if (schemaDocuments[uriString]) { + client.sendNotification(SchemaContentChangeNotification.type, uriString); + return true; + } + return false; + }; + const handleActiveEditorChange = (activeEditor?: TextEditor) => { + if (!activeEditor) { + return; + } + + const activeDocUri = activeEditor.document.uri.toString(); + + if (activeDocUri && fileSchemaErrors.has(activeDocUri)) { + schemaResolutionErrorStatusBarItem.show(); + } else { + schemaResolutionErrorStatusBarItem.hide(); + } + }; + + toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); + toDispose.push(workspace.onDidCloseTextDocument(d => { + const uriString = d.uri.toString(); + if (handleContentChange(uriString)) { + delete schemaDocuments[uriString]; + } + fileSchemaErrors.delete(uriString); + })); + toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); + + const handleRetryResolveSchemaCommand = () => { + if (window.activeTextEditor) { + schemaResolutionErrorStatusBarItem.text = '$(watch)'; + const activeDocUri = window.activeTextEditor.document.uri.toString(); + client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => { + const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); + if (schemaErrorIndex !== -1) { + // Show schema resolution errors in status bar only; ref: #51032 + const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; + fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message); + } else { + schemaResolutionErrorStatusBarItem.hide(); + } + schemaResolutionErrorStatusBarItem.text = '$(alert)'; + }); + } + }; + + toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand)); + + client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); + + 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(); + toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); + + updateSchemaDownloadSetting(); + + toDispose.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(SettingIds.enableFormatter)) { + updateFormatterRegistration(); + } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { + updateSchemaDownloadSetting(); + } + })); + + client.onNotification(ResultLimitReachedNotification.type, async message => { + const shouldPrompt = context.globalState.get(StorageIds.maxItemsExceededInformation) !== false; + if (shouldPrompt) { + 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); + if (pick === neverAgain) { + await context.globalState.update(StorageIds.maxItemsExceededInformation, false); + } else if (pick === openSettings) { + await commands.executeCommand('workbench.action.openSettings', SettingIds.maxItemsComputed); + } + } + }); + + toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri))); + + function updateFormatterRegistration() { + const formatEnabled = workspace.getConfiguration().get(SettingIds.enableFormatter); + if (!formatEnabled && rangeFormatting) { + rangeFormatting.dispose(); + rangeFormatting = undefined; + } else if (formatEnabled && !rangeFormatting) { + rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, { + 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) + }; + + return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( + client.protocol2CodeConverter.asTextEdits, + (error) => { + client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []); + return Promise.resolve([]); + } + ); + } + }); + } + } + + function updateSchemaDownloadSetting() { + schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false; + if (schemaDownloadEnabled) { + schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionErrorMessage', 'Unable to resolve schema. Click to retry.'); + schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema'; + handleRetryResolveSchemaCommand(); + } else { + schemaResolutionErrorStatusBarItem.tooltip = localize('json.schemaResolutionDisabledMessage', 'Downloading schemas is disabled. Click to configure.'); + schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' }; + } + } + + return client; } function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { diff --git a/extensions/json-language-features/client/src/node/jsonClientMain.ts b/extensions/json-language-features/client/src/node/jsonClientMain.ts index 9809074a5a9..752abe59a9f 100644 --- a/extensions/json-language-features/client/src/node/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/node/jsonClientMain.ts @@ -5,7 +5,7 @@ import { ExtensionContext, OutputChannel, window, workspace } from 'vscode'; import { startClient, LanguageClientConstructor, SchemaRequestService, languageServerDescription } from '../jsonClient'; -import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient, BaseLanguageClient } from 'vscode-languageclient/node'; import { promises as fs } from 'fs'; import * as path from 'path'; @@ -15,6 +15,7 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import { JSONSchemaCache } from './schemaCache'; let telemetry: TelemetryReporter | undefined; +let client: BaseLanguageClient | undefined; // this method is called when vs code is activated export async function activate(context: ExtensionContext) { @@ -45,11 +46,15 @@ export async function activate(context: ExtensionContext) { const schemaRequests = await getSchemaRequestService(context, log); - startClient(context, newLanguageClient, { schemaRequests, telemetry }); + client = await startClient(context, newLanguageClient, { schemaRequests, telemetry }); } -export function deactivate(): Promise { - return telemetry ? telemetry.dispose() : Promise.resolve(null); +export async function deactivate(): Promise { + if (client) { + await client.stop(); + client = undefined; + } + telemetry?.dispose(); } interface IPackageInfo { diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 25aef750a76..1fc6753efbb 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -147,10 +147,10 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "0.5.0", + "@vscode/extension-telemetry": "0.5.1", "request-light": "^0.5.8", - "vscode-languageclient": "^7.0.0", - "vscode-nls": "^5.0.0" + "vscode-languageclient": "^8.0.1", + "vscode-nls": "^5.0.1" }, "devDependencies": { "@types/node": "16.x" diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index cd04f24c3ab..a60bc177dc1 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,8 +14,8 @@ "dependencies": { "jsonc-parser": "^3.0.0", "request-light": "^0.5.8", - "vscode-json-languageservice": "^4.2.1", - "vscode-languageserver": "^7.0.0", + "vscode-json-languageservice": "^5.0.0", + "vscode-languageserver": "^8.0.1", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index e105859371f..9594d242f51 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -6,11 +6,12 @@ import { Connection, TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions, Diagnostic } from 'vscode-languageserver'; -import { formatError, runSafe, runSafeAsync } from './utils/runner'; -import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Diagnostic, Range, Position } from 'vscode-json-languageservice'; +import { runSafe, runSafeAsync } from './utils/runner'; +import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; +import { TextDocument, JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities, Range, Position } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; import { Utils, URI } from 'vscode-uri'; @@ -113,11 +114,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let resultLimit = Number.MAX_VALUE; let formatterMaxNumberOfEdits = Number.MAX_VALUE; + let diagnosticsSupport: DiagnosticsSupport | undefined; + + // After the server has started the client sends an initialize request. The server receives // in the passed params the rootPath of the workspace plus the client capabilities. connection.onInitialize((params: InitializeParams): InitializeResult => { - const handledProtocols = params.initializationOptions?.handledSchemaProtocols; + const initializationOptions = params.initializationOptions as any || {}; + + const handledProtocols = initializationOptions?.handledSchemaProtocols; languageService = getLanguageService({ schemaRequestService: getSchemaRequestService(handledProtocols), @@ -139,10 +145,18 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); - dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions?.provideFormatter !== 'boolean'); + dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions.provideFormatter !== 'boolean'); foldingRangeLimitDefault = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); hierarchicalDocumentSymbolSupport = getClientCapability('textDocument.documentSymbol.hierarchicalDocumentSymbolSupport', false); - formatterMaxNumberOfEdits = params.initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; + formatterMaxNumberOfEdits = initializationOptions.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; + + const supportsDiagnosticPull = getClientCapability('textDocument.diagnostic', undefined); + if (supportsDiagnosticPull === undefined) { + diagnosticsSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); + } else { + diagnosticsSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); + } + const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, completionProvider: clientSnippetSupport ? { @@ -151,12 +165,17 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } : undefined, hoverProvider: true, documentSymbolProvider: true, - documentRangeFormattingProvider: params.initializationOptions?.provideFormatter === true, - documentFormattingProvider: params.initializationOptions?.provideFormatter === true, + documentRangeFormattingProvider: initializationOptions.provideFormatter === true, + documentFormattingProvider: initializationOptions.provideFormatter === true, colorProvider: {}, foldingRangeProvider: true, selectionRangeProvider: true, - documentLinkProvider: {} + documentLinkProvider: {}, + diagnosticProvider: { + documentSelector: null, + interFileDependencies: false, + workspaceDiagnostics: false + } }; return { capabilities }; @@ -279,25 +298,18 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) needsRevalidation = languageService.resetSchema(uriOrUris); } if (needsRevalidation) { - for (const doc of documents.all()) { - triggerValidation(doc); - } + diagnosticsSupport?.requestRefresh(); } }); // Retry schema validation on all open documents - connection.onRequest(ForceValidateRequest.type, uri => { - return new Promise(resolve => { - const document = documents.get(uri); - if (document) { - updateConfiguration(); - validateTextDocument(document, diagnostics => { - resolve(diagnostics); - }); - } else { - resolve([]); - } - }); + connection.onRequest(ForceValidateRequest.type, async uri => { + const document = documents.get(uri); + if (document) { + updateConfiguration(); + return await validateTextDocument(document); + } + return []; }); connection.onRequest(LanguageStatusRequest.type, async uri => { @@ -343,72 +355,16 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } languageService.configure(languageSettings); - // Revalidate any open text documents - documents.all().forEach(triggerValidation); + diagnosticsSupport?.requestRefresh(); } - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent((change) => { - limitExceededWarnings.cancel(change.document.uri); - triggerValidation(change.document); - }); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - limitExceededWarnings.cancel(event.document.uri); - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }); - - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 300; - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - if (validateEnabled) { - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); - } else { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] }); - } - } - - function validateTextDocument(textDocument: TextDocument, callback?: (diagnostics: Diagnostic[]) => void): void { - const respond = (diagnostics: Diagnostic[]) => { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - if (callback) { - callback(diagnostics); - } - }; + async function validateTextDocument(textDocument: TextDocument): Promise { if (textDocument.getText().length === 0) { - respond([]); // ignore empty documents - return; + return []; // ignore empty documents } const jsonDocument = getJSONDocument(textDocument); - const version = textDocument.version; - const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; - languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => { - runtime.timer.setImmediate(() => { - const currDocument = documents.get(textDocument.uri); - if (currDocument && currDocument.version === version) { - respond(diagnostics); // Send the computed diagnostics to VSCode. - } - }); - }, error => { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, error)); - }); + return await languageService.doValidation(textDocument, jsonDocument, documentSettings); } connection.onDidChangeWatchedFiles((change) => { @@ -420,7 +376,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } }); if (hasChanges) { - documents.all().forEach(triggerValidation); + diagnosticsSupport?.requestRefresh(); } }); diff --git a/extensions/json-language-features/server/src/utils/validation.ts b/extensions/json-language-features/server/src/utils/validation.ts new file mode 100644 index 00000000000..a4d06d90cd6 --- /dev/null +++ b/extensions/json-language-features/server/src/utils/validation.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-json-languageservice'; +import { formatError, runSafeAsync } from './runner'; +import { RuntimeEnvironment } from '../jsonServer'; + +export type Validator = (textDocument: TextDocument) => Promise; +export type DiagnosticsSupport = { + dispose(): void; + requestRefresh(): void; +}; + +export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + const pendingValidationRequests: { [uri: string]: Disposable } = {}; + const validationDelayMs = 500; + + const disposables: Disposable[] = []; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }, undefined, disposables); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }, undefined, disposables); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + request.dispose(); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { + if (request === pendingValidationRequests[textDocument.uri]) { + try { + const diagnostics = await validate(textDocument); + if (request === pendingValidationRequests[textDocument.uri]) { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + } + delete pendingValidationRequests[textDocument.uri]; + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + }, validationDelayMs); + } + + return { + requestRefresh: () => { + documents.all().forEach(triggerValidation); + }, + dispose: () => { + disposables.forEach(d => d.dispose()); + disposables.length = 0; + const keys = Object.keys(pendingValidationRequests); + for (const key of keys) { + pendingValidationRequests[key].dispose(); + delete pendingValidationRequests[key]; + } + } + }; +} + +export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { + + function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { + return { + kind: DocumentDiagnosticReportKind.Full, + items: diagnostics + }; + } + + const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { + return runSafeAsync(runtime, async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + return newDocumentDiagnosticReport(await validate(document)); + } + return newDocumentDiagnosticReport([]); + + }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); + }); + + function requestRefresh(): void { + connection.languages.diagnostics.refresh(); + } + + return { + requestRefresh, + dispose: () => { + registration.dispose(); + } + }; + +} diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 5cd1cf5f480..2e74a77e9d3 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -22,51 +22,51 @@ request-light@^0.5.8: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.8.tgz#8bf73a07242b9e7b601fac2fa5dc22a094abcc27" integrity sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg== -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== +vscode-json-languageservice@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.0.0.tgz#465d76cfe5dfeed4c3d5a2123b50e3f115bb7f78" + integrity sha512-1/+1TJBRFrfCNizmrW0fbIvguKzzO+4ehlqWCCnF7ioSACUGHrYop4ANb+eRnFaCP6fi3+i+llJC5Y5yAvmL6w== dependencies: jsonc-parser "^3.0.0" - vscode-languageserver-textdocument "^1.0.3" - vscode-languageserver-types "^3.16.0" - vscode-nls "^5.0.0" + vscode-languageserver-textdocument "^1.0.4" + vscode-languageserver-types "^3.17.1" + vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" + vscode-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" -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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1, vscode-languageserver-types@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-languageserver@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" - integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== +vscode-languageserver@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-8.0.1.tgz#56bd7a01f5c88af075a77f1d220edcb30fc4bdc7" + integrity sha512-sn7SjBwWm3OlmLtgg7jbM0wBULppyL60rj8K5HF0ny/MzN+GzPBX1kCvYdybhl7UW63V5V5tRVnyB8iwC73lSQ== dependencies: - vscode-languageserver-protocol "3.16.0" + vscode-languageserver-protocol "3.17.1" -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== vscode-uri@^3.0.3: version "3.0.3" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 98216da36e6..3b6ad62624a 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@vscode/extension-telemetry@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.5.0.tgz#8214171e550393d577fc56326fa986c6800b831b" - integrity sha512-27FsgeVJvC4zVw7Ar3Ub+7vJswDt8RoBFpbgBwf8Xq/B2gaT8G6a+gkw3s2pQmjWGIqyu7TRA8e9rS8/vxv6NQ== +"@vscode/extension-telemetry@0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.5.1.tgz#20150976629663b3d33799a4ad25944a1535f7db" + integrity sha512-cvFq8drxdLRF8KN72WcV4lTEa9GqDiRwy9EbnYuoSCD9Jdk8zHFF49MmACC1qs4R9Ko/C1uMOmeLJmVi8EA0rQ== balanced-match@^1.0.0: version "1.0.0" @@ -49,44 +49,44 @@ request-light@^0.5.8: resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.8.tgz#8bf73a07242b9e7b601fac2fa5dc22a094abcc27" integrity sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg== -semver@^7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== +vscode-jsonrpc@8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.1.tgz#f30b0625ebafa0fb3bc53e934ca47b706445e57e" + integrity sha512-N/WKvghIajmEvXpatSzvTvOIz61ZSmOSa4BRA4pTLi+1+jozquQKP/MkaylP9iB68k73Oua1feLQvH3xQuigiQ== -vscode-languageclient@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" - integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== +vscode-languageclient@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-8.0.1.tgz#bf5535c4463a78daeaca0bcb4f5868aec86bb301" + integrity sha512-9XoE+HJfaWvu7Y75H3VmLo5WLCtsbxEgEhrLPqwt7eyoR49lUIyyrjb98Yfa50JCMqF2cePJAEVI6oe2o1sIhw== dependencies: minimatch "^3.0.4" - semver "^7.3.4" - vscode-languageserver-protocol "3.16.0" + semver "^7.3.5" + vscode-languageserver-protocol "3.17.1" -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== +vscode-languageserver-protocol@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.1.tgz#e801762c304f740208b6c804a0cf21f2c87509ed" + integrity sha512-BNlAYgQoYwlSgDLJhSG+DeA8G1JyECqRzM2YO6tMmMji3Ad9Mw6AW7vnZMti90qlAKb0LqAlJfSVGEdqMMNzKg== dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" + vscode-jsonrpc "8.0.1" + vscode-languageserver-types "3.17.1" -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" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== +vscode-languageserver-types@3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.1.tgz#c2d87fa7784f8cac389deb3ff1e2d9a7bef07e16" + integrity sha512-K3HqVRPElLZVVPtMeKlsyL9aK0GxGQpvtAUTfX4k7+iJ4mc1M+JM+zQwkgGy2LzY0f0IAafe8MKqIkJrxfGGjQ== -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== +vscode-nls@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.1.tgz#ba23fc4d4420d25e7f886c8e83cbdcec47aa48b2" + integrity sha512-hHQV6iig+M21lTdItKPkJAaWrxALQb/nqpVffakO4knJOh3DrU2SXOMzUzNgo1eADPzu3qSsJY1weCzvR52q9A== yallist@^4.0.0: version "4.0.0" diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index 3fdb86bd8d7..a036374fed1 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -9,7 +9,57 @@ import type * as MarkdownItToken from 'markdown-it/lib/token'; import type { ActivationFunction } from 'vscode-notebook-renderer'; const sanitizerOptions: DOMPurify.Config = { - ALLOWED_TAGS: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], + ALLOWED_TAGS: [ + 'a', + 'b', + 'blockquote', + 'br', + 'button', + 'caption', + 'center', + 'code', + 'col', + 'colgroup', + 'details', + 'div', + 'em', + 'font', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'input', + 'kbd', + 'label', + 'li', + 'ol', + 'p', + 'pre', + 'select', + 'small', + 'span', + 'strong', + 'sub', + 'summary', + 'sup', + 'table', + 'tbody', + 'td', + 'textarea', + 'tfoot', + 'th', + 'thead', + 'tr', + 'tt', + 'u', + 'ul', + 'video', + ], }; export const activate: ActivationFunction = (ctx) => { @@ -207,7 +257,9 @@ export const activate: ActivationFunction = (ctx) => { previewNode.classList.remove('emptyMarkdownCell'); const markdownText = outputInfo.mime.startsWith('text/x-') ? `\`\`\`${outputInfo.mime.substr(7)}\n${text}\n\`\`\`` : (outputInfo.mime.startsWith('application/') ? `\`\`\`${outputInfo.mime.substr(12)}\n${text}\n\`\`\`` : text); - const unsanitizedRenderedMarkdown = markdownIt.render(markdownText); + const unsanitizedRenderedMarkdown = markdownIt.render(markdownText, { + outputItem: outputInfo, + }); previewNode.innerHTML = (ctx.workspace.isTrusted ? unsanitizedRenderedMarkdown : DOMPurify.sanitize(unsanitizedRenderedMarkdown, sanitizerOptions)) as string; diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 1af496d0fce..f2ad883243f 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -16,7 +16,8 @@ "Programming Languages" ], "enabledApiProposals": [ - "textEditorDrop" + "textEditorDrop", + "documentPaste" ], "activationEvents": [ "onLanguage:markdown", @@ -414,16 +415,22 @@ "markdownDescription": "%configuration.markdown.editor.drop.enabled%", "scope": "resource" }, + "markdown.experimental.editor.pasteLinks.enabled": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.markdown.editor.pasteLinks.enabled%", + "scope": "resource" + }, "markdown.experimental.validate.enabled": { "type": "boolean", "scope": "resource", "description": "%configuration.markdown.experimental.validate.enabled.description%", "default": false }, - "markdown.experimental.validate.referenceLinks": { + "markdown.experimental.validate.referenceLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.referenceLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.referenceLinks.enabled.description%", "default": "warning", "enum": [ "ignore", @@ -431,10 +438,10 @@ "error" ] }, - "markdown.experimental.validate.headerLinks": { + "markdown.experimental.validate.headerLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.headerLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.headerLinks.enabled.description%", "default": "warning", "enum": [ "ignore", @@ -442,16 +449,24 @@ "error" ] }, - "markdown.experimental.validate.fileLinks": { + "markdown.experimental.validate.fileLinks.enabled": { "type": "string", "scope": "resource", - "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.description%", + "markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.enabled.description%", "default": "warning", "enum": [ "ignore", "warning", "error" ] + }, + "markdown.experimental.validate.ignoreLinks": { + "type": "array", + "scope": "resource", + "markdownDescription": "%configuration.markdown.experimental.validate.ignoreLinks.description%", + "items": { + "type": "string" + } } } }, @@ -504,6 +519,7 @@ "markdown-it": "^12.3.2", "markdown-it-front-matter": "^0.2.1", "morphdom": "^2.6.1", + "picomatch": "^2.3.1", "vscode-languageserver-textdocument": "^1.0.4", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.3" @@ -512,6 +528,7 @@ "@types/dompurify": "^2.3.1", "@types/lodash.throttle": "^4.1.3", "@types/markdown-it": "12.2.3", + "@types/picomatch": "^2.3.0", "@types/vscode-notebook-renderer": "^1.60.0", "@types/vscode-webview": "^1.57.0", "lodash.throttle": "^4.1.1" diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index 2d9ba258003..1f3598e3fd8 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -29,9 +29,11 @@ "configuration.markdown.links.openLocation.beside": "Open links beside the active editor.", "configuration.markdown.suggest.paths.enabled.description": "Enable/disable path suggestions for markdown links", "configuration.markdown.editor.drop.enabled": "Enable/disable dropping into the markdown editor to insert shift. Requires enabling `#workbenck.experimental.editor.dropIntoEditor.enabled#`.", + "configuration.markdown.editor.pasteLinks.enabled": "Enable/disable pasting files into a Markdown editor inserts Markdown links.", "configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.", - "configuration.markdown.experimental.validate.referenceLinks.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.", - "configuration.markdown.experimental.validate.headerLinks.description": "Validate links to headers in Markdown files, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.", - "configuration.markdown.experimental.validate.fileLinks.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.experimental.validate.enabled#`.", + "configuration.markdown.experimental.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.", + "configuration.markdown.experimental.validate.headerLinks.enabled.description": "Validate links to headers in Markdown files, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.", + "configuration.markdown.experimental.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.experimental.validate.enabled#`.", + "configuration.markdown.experimental.validate.ignoreLinks.description": "Configure links that should not be validated. For example `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.", "workspaceTrust": "Required for loading styles configured in the workspace." } diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 4938c1004bf..0f2399692e9 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -6,8 +6,9 @@ import * as vscode from 'vscode'; import { CommandManager } from './commandManager'; import * as commands from './commands/index'; -import { register as registerDiagnostics } from './languageFeatures/diagnostics'; +import { registerPasteProvider } from './languageFeatures/copyPaste'; import { MdDefinitionProvider } from './languageFeatures/definitionProvider'; +import { register as registerDiagnostics } from './languageFeatures/diagnostics'; import { MdLinkProvider } from './languageFeatures/documentLinkProvider'; import { MdDocumentSymbolProvider } from './languageFeatures/documentSymbolProvider'; import { registerDropIntoEditor } from './languageFeatures/dropIntoEditor'; @@ -76,8 +77,9 @@ function registerMarkdownLanguageFeatures( vscode.languages.registerRenameProvider(selector, new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier)), vscode.languages.registerDefinitionProvider(selector, new MdDefinitionProvider(referencesProvider)), MdPathCompletionProvider.register(selector, engine, linkProvider), - registerDiagnostics(engine, workspaceContents, linkProvider), + registerDiagnostics(selector, engine, workspaceContents, linkProvider, commandManager), registerDropIntoEditor(selector), + registerPasteProvider(selector), registerFindFileReferences(commandManager, referencesProvider), ); } diff --git a/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts b/extensions/markdown-language-features/src/languageFeatures/copyPaste.ts new file mode 100644 index 00000000000..d9e939b463c --- /dev/null +++ b/extensions/markdown-language-features/src/languageFeatures/copyPaste.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 * as vscode from 'vscode'; +import { tryInsertUriList } from './dropIntoEditor'; + +export function registerPasteProvider(selector: vscode.DocumentSelector) { + return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider { + + async provideDocumentPasteEdits( + document: vscode.TextDocument, + range: vscode.Range, + dataTransfer: vscode.DataTransfer, + token: vscode.CancellationToken, + ): Promise { + const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', false); + if (!enabled) { + return; + } + + return tryInsertUriList(document, range, dataTransfer, token); + } + }); +} diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index fd11cce6531..10547f2000e 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as picomatch from 'picomatch'; import { MarkdownEngine } from '../markdownEngine'; import { TableOfContents } from '../tableOfContents'; import { Delayer } from '../util/async'; @@ -12,8 +13,9 @@ import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; import { Limiter } from '../util/limiter'; import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents'; -import { LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinkProvider'; +import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinkProvider'; import { tryFindMdDocumentForLink } from './references'; +import { CommandManager } from '../commandManager'; const localize = nls.loadMessageBundle(); @@ -37,6 +39,7 @@ export interface DiagnosticOptions { readonly validateReferences: DiagnosticLevel; readonly validateOwnHeaders: DiagnosticLevel; readonly validateFilePaths: DiagnosticLevel; + readonly ignoreLinks: readonly string[]; } function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined { @@ -56,7 +59,13 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf super(); this._register(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('markdown.experimental.validate.enabled')) { + if ( + e.affectsConfiguration('markdown.experimental.validate.enabled') + || e.affectsConfiguration('markdown.experimental.validate.referenceLinks.enabled') + || e.affectsConfiguration('markdown.experimental.validate.headerLinks.enabled') + || e.affectsConfiguration('markdown.experimental.validate.fileLinks.enabled') + || e.affectsConfiguration('markdown.experimental.validate.ignoreLinks') + ) { this._onDidChange.fire(); } })); @@ -66,9 +75,10 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf const config = vscode.workspace.getConfiguration('markdown', resource); return { enabled: config.get('experimental.validate.enabled', false), - validateReferences: config.get('experimental.validate.referenceLinks', DiagnosticLevel.ignore), - validateOwnHeaders: config.get('experimental.validate.headerLinks', DiagnosticLevel.ignore), - validateFilePaths: config.get('experimental.validate.fileLinks', DiagnosticLevel.ignore), + validateReferences: config.get('experimental.validate.referenceLinks.enabled', DiagnosticLevel.ignore), + validateOwnHeaders: config.get('experimental.validate.headerLinks.enabled', DiagnosticLevel.ignore), + validateFilePaths: config.get('experimental.validate.fileLinks.enabled', DiagnosticLevel.ignore), + ignoreLinks: config.get('experimental.validate.ignoreLinks', []), }; } } @@ -118,6 +128,104 @@ class InflightDiagnosticRequests { } } +class LinkWatcher extends Disposable { + + private readonly _onDidChangeLinkedToFile = this._register(new vscode.EventEmitter>); + /** + * Event fired with a list of document uri when one of the links in the document changes + */ + public readonly onDidChangeLinkedToFile = this._onDidChangeLinkedToFile.event; + + private readonly _watchers = new Map; + }>(); + + override dispose() { + super.dispose(); + + for (const entry of this._watchers.values()) { + entry.watcher.dispose(); + } + this._watchers.clear(); + } + + /** + * Set the known links in a markdown document, adding and removing file watchers as needed + */ + updateLinksForDocument(document: vscode.Uri, links: readonly MdLink[]) { + const linkedToResource = new Set( + links + .filter(link => link.href.kind === 'internal') + .map(link => (link.href as InternalHref).path)); + + // First decrement watcher counter for previous document state + for (const entry of this._watchers.values()) { + entry.documents.delete(document.toString()); + } + + // Then create/update watchers for new document state + for (const path of linkedToResource) { + let entry = this._watchers.get(path.toString()); + if (!entry) { + entry = { + watcher: this.startWatching(path), + documents: new Map(), + }; + this._watchers.set(path.toString(), entry); + } + + entry.documents.set(document.toString(), document); + } + + // Finally clean up watchers for links that are no longer are referenced anywhere + for (const [key, value] of this._watchers) { + if (value.documents.size === 0) { + value.watcher.dispose(); + this._watchers.delete(key); + } + } + } + + deleteDocument(resource: vscode.Uri) { + this.updateLinksForDocument(resource, []); + } + + private startWatching(path: vscode.Uri): vscode.Disposable { + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(path, '*'), false, true, false); + const handler = (resource: vscode.Uri) => this.onLinkedResourceChanged(resource); + return vscode.Disposable.from( + watcher, + watcher.onDidDelete(handler), + watcher.onDidCreate(handler), + ); + } + + private onLinkedResourceChanged(resource: vscode.Uri) { + const entry = this._watchers.get(resource.toString()); + if (entry) { + this._onDidChangeLinkedToFile.fire(entry.documents.values()); + } + } +} + +class LinkDoesNotExistDiagnostic extends vscode.Diagnostic { + + public readonly link: string; + + constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, link: string) { + super(range, message, severity); + this.link = link; + } +} + export class DiagnosticManager extends Disposable { private readonly collection: vscode.DiagnosticCollection; @@ -126,6 +234,8 @@ export class DiagnosticManager extends Disposable { private readonly pendingDiagnostics = new Set(); private readonly inFlightDiagnostics = this._register(new InflightDiagnosticRequests()); + private readonly linkWatcher = this._register(new LinkWatcher()); + constructor( private readonly computer: DiagnosticComputer, private readonly configuration: DiagnosticConfiguration, @@ -148,10 +258,20 @@ export class DiagnosticManager extends Disposable { this.triggerDiagnostics(e.document); })); - this._register(vscode.workspace.onDidCloseTextDocument(doc => { - this.pendingDiagnostics.delete(doc.uri); - this.inFlightDiagnostics.cancel(doc.uri); - this.collection.delete(doc.uri); + this._register(vscode.workspace.onDidCloseTextDocument(({ uri }) => { + this.pendingDiagnostics.delete(uri); + this.inFlightDiagnostics.cancel(uri); + this.linkWatcher.deleteDocument(uri); + this.collection.delete(uri); + })); + + this._register(this.linkWatcher.onDidChangeLinkedToFile(changedDocuments => { + for (const resource of changedDocuments) { + const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === resource.toString()); + if (doc) { + this.triggerDiagnostics(doc); + } + } })); this.rebuild(); @@ -162,12 +282,12 @@ export class DiagnosticManager extends Disposable { this.pendingDiagnostics.clear(); } - public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise { + public async recomputeDiagnosticState(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise<{ diagnostics: readonly vscode.Diagnostic[]; links: readonly MdLink[]; config: DiagnosticOptions }> { const config = this.configuration.getOptions(doc.uri); if (!config.enabled) { - return []; + return { diagnostics: [], links: [], config }; } - return this.computer.getDiagnostics(doc, config, token); + return { ...await this.computer.getDiagnostics(doc, config, token), config }; } private async recomputePendingDiagnostics(): Promise { @@ -178,8 +298,9 @@ export class DiagnosticManager extends Disposable { const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath); if (doc) { this.inFlightDiagnostics.trigger(doc.uri, async (token) => { - const diagnostics = await this.getDiagnostics(doc, token); - this.collection.set(doc.uri, diagnostics); + const state = await this.recomputeDiagnosticState(doc, token); + this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFilePaths ? state.links : []); + this.collection.set(doc.uri, state.diagnostics); }); } } @@ -269,17 +390,20 @@ export class DiagnosticComputer { private readonly linkProvider: MdLinkProvider, ) { } - public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise { + public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<{ readonly diagnostics: vscode.Diagnostic[]; readonly links: MdLink[] }> { const links = await this.linkProvider.getAllLinks(doc, token); if (token.isCancellationRequested) { - return []; + return { links, diagnostics: [] }; } - return (await Promise.all([ - this.validateFileLinks(doc, options, links, token), - Array.from(this.validateReferenceLinks(options, links)), - this.validateOwnHeaderLinks(doc, options, links, token), - ])).flat(); + return { + links, + diagnostics: (await Promise.all([ + this.validateFileLinks(doc, options, links, token), + Array.from(this.validateReferenceLinks(options, links)), + this.validateOwnHeaderLinks(doc, options, links, token), + ])).flat() + }; } private async validateOwnHeaderLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise { @@ -300,10 +424,13 @@ export class DiagnosticComputer { && link.href.fragment && !toc.lookup(link.href.fragment) ) { - diagnostics.push(new vscode.Diagnostic( - link.source.hrefRange, - localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment), - severity)); + if (!this.isIgnoredLink(options, link.source.text)) { + diagnostics.push(new LinkDoesNotExistDiagnostic( + link.source.hrefRange, + localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment), + severity, + link.source.text)); + } } } @@ -355,9 +482,11 @@ export class DiagnosticComputer { } if (!hrefDoc && !await this.workspaceContents.pathExists(path)) { - const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.toString(true)); + const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath); for (const link of links) { - diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity)); + if (!this.isIgnoredLink(options, link.source.pathText)) { + diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.pathText)); + } } } else if (hrefDoc) { // Validate each of the links to headers in the file @@ -365,9 +494,9 @@ export class DiagnosticComputer { if (fragmentLinks.length) { const toc = await TableOfContents.create(this.engine, hrefDoc); for (const link of fragmentLinks) { - if (!toc.lookup(link.fragment)) { + if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) { const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment); - diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity)); + diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.text)); } } } @@ -376,14 +505,70 @@ export class DiagnosticComputer { })); return diagnostics; } + + private isIgnoredLink(options: DiagnosticOptions, link: string): boolean { + return options.ignoreLinks.some(glob => picomatch.isMatch(link, glob)); + } +} + +class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider { + + private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks'; + + private static readonly metadata: vscode.CodeActionProviderMetadata = { + providedCodeActionKinds: [ + vscode.CodeActionKind.QuickFix + ], + }; + + public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable { + const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.metadata); + const commandReg = commandManager.register({ + id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + execute(resource: vscode.Uri, path: string) { + const settingId = 'experimental.validate.ignoreLinks'; + const config = vscode.workspace.getConfiguration('markdown', resource); + const paths = new Set(config.get(settingId, [])); + paths.add(path); + config.update(settingId, [...paths], vscode.ConfigurationTarget.WorkspaceFolder); + } + }); + return vscode.Disposable.from(reg, commandReg); + } + + provideCodeActions(document: vscode.TextDocument, _range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> { + const fixes: vscode.CodeAction[] = []; + + for (const diagnostic of context.diagnostics) { + if (diagnostic instanceof LinkDoesNotExistDiagnostic) { + const fix = new vscode.CodeAction( + localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", diagnostic.link), + vscode.CodeActionKind.QuickFix); + + fix.command = { + command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId, + title: '', + arguments: [document.uri, diagnostic.link] + }; + fixes.push(fix); + } + } + + return fixes; + } } export function register( + selector: vscode.DocumentSelector, engine: MarkdownEngine, workspaceContents: MdWorkspaceContents, linkProvider: MdLinkProvider, + commandManager: CommandManager, ): vscode.Disposable { const configuration = new VSCodeDiagnosticConfiguration(); const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration); - return vscode.Disposable.from(configuration, manager); + return vscode.Disposable.from( + configuration, + manager, + AddToIgnoreLinksQuickFixProvider.register(selector, commandManager)); } diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts index 56195d7a03c..e58094d7b7f 100644 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts @@ -93,7 +93,16 @@ function getWorkspaceFolder(document: SkinnyTextDocument) { } export interface MdLinkSource { + /** + * The original text of the link destination in code. + */ readonly text: string; + + /** + * The original text of just the link's path in code. + */ + readonly pathText: string; + readonly resource: vscode.Uri; readonly hrefRange: vscode.Range; readonly fragmentRange: vscode.Range | undefined; @@ -138,7 +147,7 @@ function extractDocumentLink( text: link, resource: document.uri, hrefRange: new vscode.Range(linkStart, linkEnd), - fragmentRange: getFragmentRange(link, linkStart, linkEnd), + ...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd), } }; } catch { @@ -154,6 +163,14 @@ function getFragmentRange(text: string, start: vscode.Position, end: vscode.Posi return new vscode.Range(start.translate({ characterDelta: index + 1 }), end); } +function getLinkSourceFragmentInfo(document: SkinnyTextDocument, link: string, linkStart: vscode.Position, linkEnd: vscode.Position): { fragmentRange: vscode.Range | undefined; pathText: string } { + const fragmentRange = getFragmentRange(link, linkStart, linkEnd); + return { + pathText: document.getText(new vscode.Range(linkStart, fragmentRange ? fragmentRange.start.translate(0, -1) : linkEnd)), + fragmentRange, + }; +} + const angleBracketLinkRe = /^<(.*)>$/; /** @@ -187,35 +204,38 @@ const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^> const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm; -interface CodeInDocument { - /** - * code blocks and fences each represented by [line_start,line_end). - */ - readonly multiline: ReadonlyArray<[number, number]>; +class NoLinkRanges { + public static async compute(document: SkinnyTextDocument, engine: MarkdownEngine): Promise { + const tokens = await engine.parse(document); + const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence' || t.type === 'html_block') && !!t.map).map(t => t.map) as [number, number][]; - /** - * inline code spans each represented by {@link vscode.Range}. - */ - readonly inline: readonly vscode.Range[]; + const text = document.getText(); + const inline = [...text.matchAll(inlineCodePattern)].map(match => { + const start = match.index || 0; + return new vscode.Range(document.positionAt(start), document.positionAt(start + match[0].length)); + }); + + return new NoLinkRanges(multiline, inline); + } + + private constructor( + /** + * code blocks and fences each represented by [line_start,line_end). + */ + public readonly multiline: ReadonlyArray<[number, number]>, + + /** + * Inline code spans where links should not be detected + */ + public readonly inline: readonly vscode.Range[] + ) { } + + contains(range: vscode.Range): boolean { + return this.multiline.some(interval => range.start.line >= interval[0] && range.start.line < interval[1]) || + this.inline.some(position => position.intersection(range)); + } } -async function findCode(document: SkinnyTextDocument, engine: MarkdownEngine): Promise { - const tokens = await engine.parse(document); - const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence') && !!t.map).map(t => t.map) as [number, number][]; - - const text = document.getText(); - const inline = [...text.matchAll(inlineCodePattern)].map(match => { - const start = match.index || 0; - return new vscode.Range(document.positionAt(start), document.positionAt(start + match[0].length)); - }); - - return { multiline, inline }; -} - -function isLinkInsideCode(code: CodeInDocument, linkHrefRange: vscode.Range) { - return code.multiline.some(interval => linkHrefRange.start.line >= interval[0] && linkHrefRange.start.line < interval[1]) || - code.inline.some(position => position.intersection(linkHrefRange)); -} export class MdLinkProvider implements vscode.DocumentLinkProvider { @@ -262,35 +282,35 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { } public async getAllLinks(document: SkinnyTextDocument, token: vscode.CancellationToken): Promise { - const codeInDocument = await findCode(document, this.engine); + const noLinkRanges = await NoLinkRanges.compute(document, this.engine); if (token.isCancellationRequested) { return []; } return Array.from([ - ...this.getInlineLinks(document, codeInDocument), - ...this.getReferenceLinks(document, codeInDocument), - ...this.getLinkDefinitions2(document, codeInDocument), - ...this.getAutoLinks(document, codeInDocument), + ...this.getInlineLinks(document, noLinkRanges), + ...this.getReferenceLinks(document, noLinkRanges), + ...this.getLinkDefinitions2(document, noLinkRanges), + ...this.getAutoLinks(document, noLinkRanges), ]); } - private *getInlineLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable { + private *getInlineLinks(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable { const text = document.getText(); for (const match of text.matchAll(linkPattern)) { const matchImageData = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index); - if (matchImageData && !isLinkInsideCode(codeInDocument, matchImageData.source.hrefRange)) { + if (matchImageData && !noLinkRanges.contains(matchImageData.source.hrefRange)) { yield matchImageData; } const matchLinkData = extractDocumentLink(document, match[1].length, match[5], match.index); - if (matchLinkData && !isLinkInsideCode(codeInDocument, matchLinkData.source.hrefRange)) { + if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange)) { yield matchLinkData; } } } - private *getAutoLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable { + private *getAutoLinks(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable { const text = document.getText(); for (const match of text.matchAll(autoLinkPattern)) { @@ -301,7 +321,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { const linkStart = document.positionAt(offset); const linkEnd = document.positionAt(offset + link.length); const hrefRange = new vscode.Range(linkStart, linkEnd); - if (isLinkInsideCode(codeInDocument, hrefRange)) { + if (noLinkRanges.contains(hrefRange)) { continue; } yield { @@ -311,14 +331,14 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { text: link, resource: document.uri, hrefRange: new vscode.Range(linkStart, linkEnd), - fragmentRange: getFragmentRange(link, linkStart, linkEnd), + ...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd), } }; } } } - private *getReferenceLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable { + private *getReferenceLinks(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable { const text = document.getText(); for (const match of text.matchAll(referenceLinkPattern)) { let linkStart: vscode.Position; @@ -339,7 +359,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { } const hrefRange = new vscode.Range(linkStart, linkEnd); - if (isLinkInsideCode(codeInDocument, hrefRange)) { + if (noLinkRanges.contains(hrefRange)) { continue; } @@ -347,6 +367,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { kind: 'link', source: { text: reference, + pathText: reference, resource: document.uri, hrefRange, fragmentRange: undefined, @@ -360,11 +381,11 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { } public async getLinkDefinitions(document: SkinnyTextDocument): Promise> { - const codeInDocument = await findCode(document, this.engine); - return this.getLinkDefinitions2(document, codeInDocument); + const noLinkRanges = await NoLinkRanges.compute(document, this.engine); + return this.getLinkDefinitions2(document, noLinkRanges); } - private *getLinkDefinitions2(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable { + private *getLinkDefinitions2(document: SkinnyTextDocument, noLinkRanges: NoLinkRanges): Iterable { const text = document.getText(); for (const match of text.matchAll(definitionPattern)) { const pre = match[1]; @@ -388,7 +409,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { text = link; } const hrefRange = new vscode.Range(linkStart, linkEnd); - if (isLinkInsideCode(codeInDocument, hrefRange)) { + if (noLinkRanges.contains(hrefRange)) { continue; } const target = parseLink(document, text); @@ -399,7 +420,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { text: link, resource: document.uri, hrefRange, - fragmentRange: getFragmentRange(link, linkStart, linkEnd), + ...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd), }, ref: { text: reference, range: refRange }, href: target, diff --git a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts index c3fb1f55631..2ad71ec0516 100644 --- a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts +++ b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts @@ -24,7 +24,7 @@ const imageFileExtensions = new Set([ ]); export function registerDropIntoEditor(selector: vscode.DocumentSelector) { - return vscode.languages.registerDocumentOnDropProvider(selector, new class implements vscode.DocumentOnDropProvider { + return vscode.languages.registerDocumentOnDropEditProvider(selector, new class implements vscode.DocumentOnDropEditProvider { async provideDocumentOnDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true); if (!enabled) { @@ -32,45 +32,45 @@ export function registerDropIntoEditor(selector: vscode.DocumentSelector) { } const replacementRange = new vscode.Range(position, position); - return this.tryInsertUriList(document, replacementRange, dataTransfer, token); - } - - private async tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { - const urlList = await dataTransfer.get('text/uri-list')?.asString(); - if (!urlList || token.isCancellationRequested) { - return undefined; - } - - const uris: vscode.Uri[] = []; - for (const resource of urlList.split('\n')) { - try { - uris.push(vscode.Uri.parse(resource)); - } catch { - // noop - } - } - - if (!uris.length) { - return; - } - - const snippet = new vscode.SnippetString(); - uris.forEach((uri, i) => { - const mdPath = document.uri.scheme === uri.scheme - ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/')) - : uri.toString(false); - - const ext = URI.Utils.extname(uri).toLowerCase(); - snippet.appendText(imageFileExtensions.has(ext) ? '![' : '['); - snippet.appendTabstop(); - snippet.appendText(`](${mdPath})`); - - if (i <= uris.length - 1 && uris.length > 1) { - snippet.appendText(' '); - } - }); - - return new vscode.SnippetTextEdit(replacementRange, snippet); + return tryInsertUriList(document, replacementRange, dataTransfer, token); } }); } + +export async function tryInsertUriList(document: vscode.TextDocument, replacementRange: vscode.Range, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise { + const urlList = await dataTransfer.get('text/uri-list')?.asString(); + if (!urlList || token.isCancellationRequested) { + return undefined; + } + + const uris: vscode.Uri[] = []; + for (const resource of urlList.split('\n')) { + try { + uris.push(vscode.Uri.parse(resource)); + } catch { + // noop + } + } + + if (!uris.length) { + return; + } + + const snippet = new vscode.SnippetString(); + uris.forEach((uri, i) => { + const mdPath = document.uri.scheme === uri.scheme + ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/')) + : uri.toString(false); + + const ext = URI.Utils.extname(uri).toLowerCase(); + snippet.appendText(imageFileExtensions.has(ext) ? '![' : '['); + snippet.appendTabstop(); + snippet.appendText(`](${mdPath})`); + + if (i <= uris.length - 1 && uris.length > 1) { + snippet.appendText(' '); + } + }); + + return new vscode.SnippetTextEdit(replacementRange, snippet); +} diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index f598dd62fa3..dd3c310a194 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -109,6 +109,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private readonly _onScrollEmitter = this._register(new vscode.EventEmitter()); public readonly onScroll = this._onScrollEmitter.event; + private readonly _disposeCts = this._register(new vscode.CancellationTokenSource()); + constructor( webview: vscode.WebviewPanel, resource: vscode.Uri, @@ -202,6 +204,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } override dispose() { + this._disposeCts.cancel(); + super.dispose(); this._disposed = true; @@ -286,7 +290,9 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { try { document = await vscode.workspace.openTextDocument(this._resource); } catch { - await this.showFileNotFoundError(); + if (!this._disposed) { + await this.showFileNotFoundError(); + } return; } @@ -306,7 +312,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { this.currentVersion = pendingVersion; const content = await (shouldReloadPage - ? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state) + ? this._contentProvider.provideTextDocumentContent(document, this, this._previewConfigurations, this.line, this.state, this._disposeCts.token) : this._contentProvider.markdownBody(document, this)); // Another call to `doUpdate` may have happened. diff --git a/extensions/markdown-language-features/src/preview/previewContentProvider.ts b/extensions/markdown-language-features/src/preview/previewContentProvider.ts index b7ff739ce91..8437017dc4f 100644 --- a/extensions/markdown-language-features/src/preview/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/preview/previewContentProvider.ts @@ -66,7 +66,8 @@ export class MarkdownContentProvider { resourceProvider: WebviewResourceProvider, previewConfigurations: MarkdownPreviewConfigurationManager, initialLine: number | undefined = undefined, - state?: any + state: any | undefined, + token: vscode.CancellationToken ): Promise { const sourceUri = markdownDocument.uri; const config = previewConfigurations.loadAndCacheConfiguration(sourceUri); @@ -89,6 +90,10 @@ export class MarkdownContentProvider { const csp = this.getCsp(resourceProvider, sourceUri, nonce); const body = await this.markdownBody(markdownDocument, resourceProvider); + if (token.isCancellationRequested) { + return { html: '', containingImages: [] }; + } + const html = ` diff --git a/extensions/markdown-language-features/src/test/diagnostic.test.ts b/extensions/markdown-language-features/src/test/diagnostic.test.ts index ce6357c65a6..3d0ffd06a6e 100644 --- a/extensions/markdown-language-features/src/test/diagnostic.test.ts +++ b/extensions/markdown-language-features/src/test/diagnostic.test.ts @@ -16,16 +16,19 @@ import { InMemoryWorkspaceMarkdownDocuments } from './inMemoryWorkspace'; import { assertRangeEqual, joinLines, workspacePath } from './util'; -function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents: MdWorkspaceContents) { +async function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents: MdWorkspaceContents): Promise { const engine = createNewMarkdownEngine(); const linkProvider = new MdLinkProvider(engine); const computer = new DiagnosticComputer(engine, workspaceContents, linkProvider); - return computer.getDiagnostics(doc, { - enabled: true, - validateFilePaths: DiagnosticLevel.warning, - validateOwnHeaders: DiagnosticLevel.warning, - validateReferences: DiagnosticLevel.warning, - }, noopToken); + return ( + await computer.getDiagnostics(doc, { + enabled: true, + validateFilePaths: DiagnosticLevel.warning, + validateOwnHeaders: DiagnosticLevel.warning, + validateReferences: DiagnosticLevel.warning, + ignoreLinks: [], + }, noopToken) + ).diagnostics; } function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration()) { @@ -41,6 +44,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration { constructor( private readonly enabled: boolean = true, + private readonly ignoreLinks: string[] = [], ) { } getOptions(_resource: vscode.Uri): DiagnosticOptions { @@ -50,6 +54,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration { validateFilePaths: DiagnosticLevel.ignore, validateOwnHeaders: DiagnosticLevel.ignore, validateReferences: DiagnosticLevel.ignore, + ignoreLinks: this.ignoreLinks, }; } return { @@ -57,6 +62,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration { validateFilePaths: DiagnosticLevel.warning, validateOwnHeaders: DiagnosticLevel.warning, validateReferences: DiagnosticLevel.warning, + ignoreLinks: this.ignoreLinks, }; } } @@ -155,7 +161,7 @@ suite('markdown: Diagnostics', () => { )); const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(false)); - const diagnostics = await manager.getDiagnostics(doc1, noopToken); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); assert.deepStrictEqual(diagnostics.length, 0); }); @@ -177,4 +183,89 @@ suite('markdown: Diagnostics', () => { const diagnostics = await getComputedDiagnostics(doc1, new InMemoryWorkspaceMarkdownDocuments([doc1])); assert.deepStrictEqual(diagnostics.length, 0); }); + + test('Should allow ignoring invalid file link using glob', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `[text](/no-such-file)`, + `![img](/no-such-file)`, + `[text]: /no-such-file`, + )); + + const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('ignoreLinks should allow skipping link to non-existent file', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `[text](/no-such-file#header)`, + )); + + const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('ignoreLinks should not consider link fragment', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `[text](/no-such-file#header)`, + )); + + const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('ignoreLinks should support globs', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `![i](/images/aaa.png)`, + `![i](/images/sub/bbb.png)`, + `![i](/images/sub/sub2/ccc.png)`, + )); + + const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/images/**/*.png'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('ignoreLinks should support ignoring header', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `![i](#no-such)`, + )); + + const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['#no-such'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('ignoreLinks should support ignoring header in file', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `![i](/doc2.md#no-such)`, + )); + const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); + + const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]); + { + const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md#no-such'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + } + { + const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md#*'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + } + }); + + test('ignoreLinks should support ignore header links if file is ignored', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `![i](/doc2.md#no-such)`, + )); + const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines('')); + + const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]); + const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md'])); + const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken); + assert.deepStrictEqual(diagnostics.length, 0); + }); }); diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index 1df40657d74..1f9960d08a1 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -270,4 +270,47 @@ suite('markdown.DocumentLinkProvider', () => { const link = links[0]; assertRangeEqual(link.range, new vscode.Range(0, 5, 0, 23)); }); + + test('Should not detect links inside html comment blocks', async () => { + const links = await getLinksForFile(joinLines( + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + )); + assert.strictEqual(links.length, 0); + }); + + test.skip('Should not detect links inside inline html comments', async () => { + // See #149678 + const links = await getLinksForFile(joinLines( + `text text`, + `text text`, + `text text`, + ``, + `text text`, + ``, + `text text`, + ``, + `text text`, + )); + assert.strictEqual(links.length, 0); + }); }); diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index 775eaa7c0a8..7c1d4a7fca8 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -7,6 +7,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDrop.d.ts", - "../../src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts" + "../../src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts", + "../../src/vscode-dts/vscode.proposed.documentPaste.d.ts" ] } diff --git a/extensions/markdown-language-features/yarn.lock b/extensions/markdown-language-features/yarn.lock index 5268b01f46c..eaa76d9abbf 100644 --- a/extensions/markdown-language-features/yarn.lock +++ b/extensions/markdown-language-features/yarn.lock @@ -39,6 +39,11 @@ resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== +"@types/picomatch@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.3.0.tgz#75db5e75a713c5a83d5b76780c3da84a82806003" + integrity sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g== + "@types/trusted-types@*": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" @@ -117,6 +122,11 @@ morphdom@^2.6.1: resolved "https://registry.yarnpkg.com/morphdom/-/morphdom-2.6.1.tgz#e868e24f989fa3183004b159aed643e628b4306e" integrity sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" diff --git a/extensions/markdown-math/package.json b/extensions/markdown-math/package.json index 8267989daee..d3bb21a553a 100644 --- a/extensions/markdown-math/package.json +++ b/extensions/markdown-math/package.json @@ -90,7 +90,7 @@ "build-notebook": "node ./esbuild" }, "dependencies": { - "@iktakahiro/markdown-it-katex": "https://github.com/mjbvz/markdown-it-katex.git" + "@iktakahiro/markdown-it-katex": "mjbvz/markdown-it-katex" }, "devDependencies": { "@types/markdown-it": "^0.0.0", diff --git a/extensions/markdown-math/yarn.lock b/extensions/markdown-math/yarn.lock index 645b0080707..2b979935781 100644 --- a/extensions/markdown-math/yarn.lock +++ b/extensions/markdown-math/yarn.lock @@ -2,9 +2,9 @@ # yarn lockfile v1 -"@iktakahiro/markdown-it-katex@https://github.com/mjbvz/markdown-it-katex.git": +"@iktakahiro/markdown-it-katex@mjbvz/markdown-it-katex": version "4.0.1" - resolved "https://github.com/mjbvz/markdown-it-katex.git#2e3736e4b916ee64ed92ebfabeaa94643612665a" + resolved "https://codeload.github.com/mjbvz/markdown-it-katex/tar.gz/1e0d09f9174b3ee1537de2586ce8d8a460284ce4" dependencies: katex "^0.13.0" diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index af53635710d..9daafb721f8 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -25,7 +25,7 @@ function generateViewMoreElement(outputId: string) { } export function truncatedArrayOfString(id: string, outputs: string[], linesLimit: number, container: HTMLElement) { - let buffer = outputs.join('\n').split(/\r|\n|\r\n/g); + let buffer = outputs.join('\n').split(/\r\n|\r|\n/g); let lineCount = buffer.length; if (lineCount < linesLimit) { diff --git a/extensions/notebook-renderers/tsconfig.json b/extensions/notebook-renderers/tsconfig.json index 2032bf87b0d..23609811f3a 100644 --- a/extensions/notebook-renderers/tsconfig.json +++ b/extensions/notebook-renderers/tsconfig.json @@ -9,7 +9,6 @@ "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", - "../../src/vscode-dts/vscode.proposed.notebookEditor.d.ts", "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts", ] } diff --git a/extensions/package.json b/extensions/package.json index 3704c9b327c..3d89442d04f 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "4.7.1-rc" + "typescript": "4.7" }, "scripts": { "postinstall": "node ./postinstall.mjs" @@ -12,6 +12,6 @@ "devDependencies": { "@parcel/watcher": "2.0.5", "esbuild": "^0.11.12", - "vscode-grammar-updater": "^1.0.4" + "vscode-grammar-updater": "^1.1.0" } } diff --git a/extensions/php/cgmanifest.json b/extensions/php/cgmanifest.json index 4b25149ffdd..4fe8f5ebcb5 100644 --- a/extensions/php/cgmanifest.json +++ b/extensions/php/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/atom/language-php", - "commitHash": "ff64523c94c014d68f5dec189b05557649c5872a" + "commitHash": "eb28b8aea1214dcbc732f3d9b9ed20c089c648bd" } }, "license": "MIT", - "version": "0.48.0" + "version": "0.48.1" } ], "version": 1 diff --git a/extensions/php/syntaxes/php.tmLanguage.json b/extensions/php/syntaxes/php.tmLanguage.json index 69a11ec2529..542b02023a7 100644 --- a/extensions/php/syntaxes/php.tmLanguage.json +++ b/extensions/php/syntaxes/php.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/atom/language-php/commit/ff64523c94c014d68f5dec189b05557649c5872a", + "version": "https://github.com/atom/language-php/commit/eb28b8aea1214dcbc732f3d9b9ed20c089c648bd", "scopeName": "source.php", "patterns": [ { @@ -13,62 +13,6 @@ { "include": "#comments" }, - { - "begin": "(?i)^\\s*(interface)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*(extends)?\\s*", - "beginCaptures": { - "1": { - "name": "storage.type.interface.php" - }, - "2": { - "name": "entity.name.type.interface.php" - }, - "3": { - "name": "storage.modifier.extends.php" - } - }, - "end": "(?i)((?:[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*\\s*,\\s*)*)([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)?\\s*(?:(?={)|$)", - "endCaptures": { - "1": { - "patterns": [ - { - "match": "(?i)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*", - "name": "entity.other.inherited-class.php" - }, - { - "match": ",", - "name": "punctuation.separator.classes.php" - } - ] - }, - "2": { - "name": "entity.other.inherited-class.php" - } - }, - "name": "meta.interface.php", - "patterns": [ - { - "include": "#namespace" - } - ] - }, - { - "begin": "(?i)^\\s*(trait)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)", - "beginCaptures": { - "1": { - "name": "storage.type.trait.php" - }, - "2": { - "name": "entity.name.type.trait.php" - } - }, - "end": "(?={)", - "name": "meta.trait.php", - "patterns": [ - { - "include": "#comments" - } - ] - }, { "match": "(?i)(?:^|(?<=<\\?php))\\s*(namespace)\\s+([a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)(?=\\s*;)", "name": "meta.namespace.php", @@ -232,6 +176,149 @@ } ] }, + { + "begin": "(?ix)\n\\b(trait)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)", + "beginCaptures": { + "1": { + "name": "storage.type.trait.php" + }, + "2": { + "name": "entity.name.type.trait.php" + } + }, + "end": "}|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.trait.end.bracket.curly.php" + } + }, + "name": "meta.trait.php", + "patterns": [ + { + "include": "#comments" + }, + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.trait.begin.bracket.curly.php" + } + }, + "end": "(?=}|\\?>)", + "contentName": "meta.trait.body.php", + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + { + "begin": "(?ix)\n\\b(interface)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)", + "beginCaptures": { + "1": { + "name": "storage.type.interface.php" + }, + "2": { + "name": "entity.name.type.interface.php" + } + }, + "end": "}|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.interface.end.bracket.curly.php" + } + }, + "name": "meta.interface.php", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#interface-extends" + }, + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.interface.begin.bracket.curly.php" + } + }, + "end": "(?=}|\\?>)", + "contentName": "meta.interface.body.php", + "patterns": [ + { + "include": "#class-constant" + }, + { + "include": "$self" + } + ] + } + ] + }, + { + "begin": "(?ix)\n\\b(enum)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\n(?: \\s* (:) \\s* (int | string) \\b )?", + "beginCaptures": { + "1": { + "name": "storage.type.enum.php" + }, + "2": { + "name": "entity.name.type.enum.php" + }, + "3": { + "name": "keyword.operator.return-value.php" + }, + "4": { + "name": "keyword.other.type.php" + } + }, + "end": "}|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.enum.end.bracket.curly.php" + } + }, + "name": "meta.enum.php", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#class-implements" + }, + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.definition.enum.begin.bracket.curly.php" + } + }, + "end": "(?=}|\\?>)", + "contentName": "meta.enum.body.php", + "patterns": [ + { + "match": "(?i)\\b(case)\\s*([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)", + "captures": { + "1": { + "name": "storage.modifier.php" + }, + "2": { + "name": "constant.enum.php" + } + } + }, + { + "include": "#class-constant" + }, + { + "include": "$self" + } + ] + } + ] + }, { "begin": "(?ix)\n(?:\n \\b(?:(abstract|final)\\s+)?(class)\\s+([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\n |\\b(new)\\b\\s*(\\#\\[.*\\])?\\s*\\b(class)\\b # anonymous class\n)", "beginCaptures": { @@ -266,89 +353,37 @@ }, "name": "meta.class.php", "patterns": [ + { + "begin": "(?<=class)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.definition.arguments.begin.bracket.round.php" + } + }, + "end": "\\)|(?=\\?>)", + "endCaptures": { + "0": { + "name": "punctuation.definition.arguments.end.bracket.round.php" + } + }, + "name": "meta.function-call.php", + "patterns": [ + { + "include": "#named-arguments" + }, + { + "include": "$self" + } + ] + }, { "include": "#comments" }, { - "begin": "(?i)(extends)\\s+", - "beginCaptures": { - "1": { - "name": "storage.modifier.extends.php" - } - }, - "contentName": "meta.other.inherited-class.php", - "end": "(?i)(?=[^a-z0-9_\\x{7f}-\\x{10ffff}\\\\])", - "patterns": [ - { - "begin": "(?i)(?=\\\\?[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*\\\\)", - "end": "(?i)([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)?(?=[^a-z0-9_\\x{7f}-\\x{10ffff}\\\\])", - "endCaptures": { - "1": { - "name": "entity.other.inherited-class.php" - } - }, - "patterns": [ - { - "include": "#namespace" - } - ] - }, - { - "include": "#class-builtin" - }, - { - "include": "#namespace" - }, - { - "match": "(?i)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*", - "name": "entity.other.inherited-class.php" - } - ] + "include": "#class-extends" }, { - "begin": "(?i)(implements)\\s+", - "beginCaptures": { - "1": { - "name": "storage.modifier.implements.php" - } - }, - "end": "(?i)(?=[;{])", - "patterns": [ - { - "include": "#comments" - }, - { - "begin": "(?i)(?=[a-z0-9_\\x{7f}-\\x{10ffff}\\\\]+)", - "contentName": "meta.other.inherited-class.php", - "end": "(?i)(?:\\s*(?:,|(?=[^a-z0-9_\\x{7f}-\\x{10ffff}\\\\\\s]))\\s*)", - "patterns": [ - { - "begin": "(?i)(?=\\\\?[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*\\\\)", - "end": "(?i)([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)?(?=[^a-z0-9_\\x{7f}-\\x{10ffff}\\\\])", - "endCaptures": { - "1": { - "name": "entity.other.inherited-class.php" - } - }, - "patterns": [ - { - "include": "#namespace" - } - ] - }, - { - "include": "#class-builtin" - }, - { - "include": "#namespace" - }, - { - "match": "(?i)[a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*", - "name": "entity.other.inherited-class.php" - } - ] - } - ] + "include": "#class-implements" }, { "begin": "{", @@ -360,6 +395,9 @@ "end": "(?=}|\\?>)", "contentName": "meta.class.body.php", "patterns": [ + { + "include": "#class-constant" + }, { "include": "$self" } @@ -382,7 +420,7 @@ } }, { - "match": "(?x)\n\\s* # FIXME: Removing this causes specs to fail. Investigate.\n\\b(\n break|case|continue|declare|default|die|do|\n else(if)?|end(declare|for(each)?|if|switch|while)|exit|\n for(each)?|if|return|switch|use|while|yield\n)\\b", + "match": "(?x)\n\\b(\n break|case|continue|declare|default|die|do|\n else(if)?|end(declare|for(each)?|if|switch|while)|exit|\n for(each)?|if|return|switch|use|while|yield\n)\\b", "captures": { "1": { "name": "keyword.control.${1:/downcase}.php" @@ -853,7 +891,7 @@ "name": "storage.type.php" }, { - "match": "(?i)\\b(global|abstract|const|extends|implements|final|private|protected|public|static)\\b", + "match": "(?i)\\b(global|abstract|const|final|private|protected|public|static)\\b", "name": "storage.modifier.php" }, { @@ -975,7 +1013,7 @@ "name": "entity.name.goto-label.php" } }, - "match": "(?i)^\\s*([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*)\\s*:(?!:)" + "match": "(?i)^\\s*([a-z_\\x{7f}-\\x{10ffff}][a-z0-9_\\x{7f}-\\x{10ffff}]*(? ...)\n \n```\nRequires using TypeScript 4.4+ in the workspace.", "comment": "The text inside the ``` block is code and should not be localized." @@ -100,6 +97,7 @@ "message": "Enable/disable inlay hints for implicit variable types:\n```typescript\n\nconst foo /* :number */ = Date.now();\n \n```\nRequires using TypeScript 4.4+ in the workspace.", "comment": "The text inside the ``` block is code and should not be localized." }, + "configuration.inlayHints.variableTypes.suppressWhenTypeMatchesName": "Suppress type hints on variables whose name is identical to the type name. Requires using TypeScript 4.8+ in the workspace.", "configuration.inlayHints.propertyDeclarationTypes.enabled": { "message": "Enable/disable inlay hints for implicit types on property declarations:\n```typescript\n\nclass Foo {\n\tprop /* :number */ = Date.now();\n}\n \n```\nRequires using TypeScript 4.4+ in the workspace.", "comment": "The text inside the ``` block is code and should not be localized." diff --git a/extensions/typescript-language-features/src/languageFeatures/completions.ts b/extensions/typescript-language-features/src/languageFeatures/completions.ts index 46a32ec68f6..a7b1e6c061c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/completions.ts +++ b/extensions/typescript-language-features/src/languageFeatures/completions.ts @@ -554,6 +554,7 @@ class CompletionAcceptedCommand implements Command { if (item instanceof MyCompletionItem) { /* __GDPR__ "completions.accept" : { + "owner": "mjbvz", "isPackageJsonImport" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "isImportStatementCompletion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "${include}": [ @@ -820,6 +821,7 @@ class TypeScriptCompletionItemProvider implements vscode.CompletionItemProvider< ) { /* __GDPR__ "completions.execute" : { + "owner": "mjbvz", "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "type" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, diff --git a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts index be91f9db4c5..c35aa35aee0 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fileConfigurationManager.ts @@ -210,6 +210,7 @@ export class InlayHintSettingNames { static readonly parameterNamesSuppressWhenArgumentMatchesName = 'inlayHints.parameterNames.suppressWhenArgumentMatchesName'; static readonly parameterNamesEnabled = 'inlayHints.parameterTypes.enabled'; static readonly variableTypesEnabled = 'inlayHints.variableTypes.enabled'; + static readonly variableTypesSuppressWhenTypeMatchesName = 'inlayHints.variableTypes.suppressWhenTypeMatchesName'; static readonly propertyDeclarationTypesEnabled = 'inlayHints.propertyDeclarationTypes.enabled'; static readonly functionLikeReturnTypesEnabled = 'inlayHints.functionLikeReturnTypes.enabled'; static readonly enumMemberValuesEnabled = 'inlayHints.enumMemberValues.enabled'; @@ -221,6 +222,7 @@ export function getInlayHintsPreferences(config: vscode.WorkspaceConfiguration) includeInlayParameterNameHintsWhenArgumentMatchesName: !config.get(InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, true), includeInlayFunctionParameterTypeHints: config.get(InlayHintSettingNames.parameterNamesEnabled, false), includeInlayVariableTypeHints: config.get(InlayHintSettingNames.variableTypesEnabled, false), + includeInlayVariableTypeHintsWhenTypeMatchesName: !config.get(InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, true), includeInlayPropertyDeclarationTypeHints: config.get(InlayHintSettingNames.propertyDeclarationTypesEnabled, false), includeInlayFunctionLikeReturnTypeHints: config.get(InlayHintSettingNames.functionLikeReturnTypesEnabled, false), includeInlayEnumMemberValueHints: config.get(InlayHintSettingNames.enumMemberValuesEnabled, false), diff --git a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts index b3a90b993b0..b0f6d257200 100644 --- a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts +++ b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts @@ -15,14 +15,15 @@ import { Position } from '../utils/typeConverters'; import FileConfigurationManager, { getInlayHintsPreferences, InlayHintSettingNames } from './fileConfigurationManager'; -const inlayHintSettingNames = [ +const inlayHintSettingNames = Object.freeze([ InlayHintSettingNames.parameterNamesSuppressWhenArgumentMatchesName, InlayHintSettingNames.parameterNamesEnabled, InlayHintSettingNames.variableTypesEnabled, + InlayHintSettingNames.variableTypesSuppressWhenTypeMatchesName, InlayHintSettingNames.propertyDeclarationTypesEnabled, InlayHintSettingNames.functionLikeReturnTypesEnabled, InlayHintSettingNames.enumMemberValuesEnabled, -]; +]); class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHintsProvider { diff --git a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts index 1cf85deb467..cd1aeffa823 100644 --- a/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts +++ b/extensions/typescript-language-features/src/languageFeatures/organizeImports.ts @@ -32,6 +32,7 @@ class OrganizeImportsCommand implements Command { public async execute(file: string, sortOnly = false): Promise { /* __GDPR__ "organizeImports.execute" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" ] diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 977bc20dc61..9ea0780a446 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -37,6 +37,7 @@ class ApplyCodeActionCommand implements Command { ): Promise { /* __GDPR__ "quickFix.execute" : { + "owner": "mjbvz", "fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" @@ -67,6 +68,7 @@ class ApplyFixAllCodeAction implements Command { public async execute(args: ApplyFixAllCodeAction_args): Promise { /* __GDPR__ "quickFixAll.execute" : { + "owner": "mjbvz", "fixName" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 392cc5a3e18..d5d1723533c 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -36,6 +36,7 @@ class DidApplyRefactoringCommand implements Command { public async execute(args: DidApplyRefactoringCommand_Args): Promise { /* __GDPR__ "refactor.execute" : { + "owner": "mjbvz", "action" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "${include}": [ "${TypeScriptCommonProperties}" diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index 12e6f70d91b..a035fbc9fdb 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -230,6 +230,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe if (!executeInfo.token || !executeInfo.token.isCancellationRequested) { /* __GDPR__ "languageServiceErrorResponse" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}", "${TypeScriptRequestErrorProperties}" diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 306dd8570eb..0fdecc5c952 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -388,6 +388,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "tsserver.spawned" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" ], @@ -418,6 +419,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "tsserver.error" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" ] @@ -443,6 +445,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType this.error(`TSServer exited with code: ${code}. Signal: ${signal}`); /* __GDPR__ "tsserver.exitWithCode" : { + "owner": "mjbvz", "code" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "signal" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "${include}": [ @@ -601,6 +604,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "serviceExited" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" ] @@ -846,6 +850,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType private fatalError(command: string, error: unknown): void { /* __GDPR__ "fatalError" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}", "${TypeScriptRequestErrorProperties}" @@ -977,6 +982,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType /* __GDPR__ "typingsInstalled" : { + "owner": "mjbvz", "installedPackages" : { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }, "installSuccess": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, "typingsInstallerVersion": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }, diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 34355b5e3e5..2690f743848 100644 --- a/extensions/typescript-language-features/src/utils/configuration.ts +++ b/extensions/typescript-language-features/src/utils/configuration.ts @@ -82,7 +82,7 @@ export class ImplicitProjectConfiguration { private static readCheckJs(configuration: vscode.WorkspaceConfiguration): boolean { return configuration.get('js/ts.implicitProjectConfig.checkJs') - ?? configuration.get('javascript.implicitProjectConfig.checkJs', true); + ?? configuration.get('javascript.implicitProjectConfig.checkJs', false); } private static readExperimentalDecorators(configuration: vscode.WorkspaceConfiguration): boolean { @@ -91,7 +91,7 @@ export class ImplicitProjectConfiguration { } private static readImplicitStrictNullChecks(configuration: vscode.WorkspaceConfiguration): boolean { - return configuration.get('js/ts.implicitProjectConfig.strictNullChecks', false); + return configuration.get('js/ts.implicitProjectConfig.strictNullChecks', true); } private static readImplicitStrictFunctionTypes(configuration: vscode.WorkspaceConfiguration): boolean { diff --git a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts index 346f95c6979..f6bdbf3721c 100644 --- a/extensions/typescript-language-features/src/utils/largeProjectStatus.ts +++ b/extensions/typescript-language-features/src/utils/largeProjectStatus.ts @@ -49,6 +49,7 @@ class ExcludeHintItem { this._item.show(); /* __GDPR__ "js.hintProjectExcludes" : { + "owner": "mjbvz", "${include}": [ "${TypeScriptCommonProperties}" ] diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 0f7f928dc81..8b2dab07a14 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -24,7 +24,6 @@ "notebookControllerKind", "notebookDebugOptions", "notebookDeprecated", - "notebookEditor", "notebookEditorDecorationType", "notebookEditorEdit", "notebookLiveShare", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts new file mode 100644 index 00000000000..6880607ca7d --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * 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 'mocha'; +import * as vscode from 'vscode'; +import { disposeAll } from '../utils'; +import { Kernel, saveAllFilesAndCloseAll } from './notebook.test'; + +export type INativeInteractiveWindow = { notebookUri: vscode.Uri; inputUri: vscode.Uri; notebookEditor: vscode.NotebookEditor }; + +async function createInteractiveWindow(kernel: Kernel) { + const { notebookEditor } = (await vscode.commands.executeCommand( + 'interactive.open', + // Keep focus on the owning file if there is one + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: false }, + undefined, + kernel.controller.id, + undefined + )) as unknown as INativeInteractiveWindow; + + return notebookEditor; +} + +async function addCell(code: string, notebook: vscode.NotebookDocument) { + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, code, 'typescript'); + const edit = vscode.NotebookEdit.insertCells(notebook.cellCount, [cell]); + const workspaceEdit = new vscode.WorkspaceEdit(); + workspaceEdit.set(notebook.uri, [edit]); + await vscode.workspace.applyEdit(workspaceEdit); + return notebook.cellAt(notebook.cellCount - 1); +} + +async function addCellAndRun(code: string, notebook: vscode.NotebookDocument) { + const cell = await addCell(code, notebook); + await vscode.commands.executeCommand('notebook.execute'); + assert.strictEqual(cell.outputs.length, 1, 'execute failed'); + return cell; +} + + +(vscode.env.uiKind === vscode.UIKind.Web ? suite.skip : suite)('Interactive Window', function () { + + const testDisposables: vscode.Disposable[] = []; + let defaultKernel: Kernel; + + setup(async function () { + // there should be ONE default kernel in this suite + defaultKernel = new Kernel('mainKernel', 'Notebook Default Kernel', 'interactive'); + testDisposables.push(defaultKernel.controller); + await saveAllFilesAndCloseAll(); + }); + + teardown(async function () { + disposeAll(testDisposables); + testDisposables.length = 0; + await saveAllFilesAndCloseAll(); + }); + + test('Can open an interactive window', async () => { + assert.ok(vscode.workspace.workspaceFolders); + const notebookEditor = await createInteractiveWindow(defaultKernel); + assert.ok(notebookEditor); + + // Try adding a cell and running it. + await addCell('print foo', notebookEditor.notebook); + + assert.strictEqual(notebookEditor.notebook.cellCount, 1); + assert.strictEqual(notebookEditor.notebook.cellAt(0).kind, vscode.NotebookCellKind.Code); + }); + + test('Interactive window scrolls after execute', async () => { + assert.ok(vscode.workspace.workspaceFolders); + const notebookEditor = await createInteractiveWindow(defaultKernel); + assert.ok(notebookEditor); + + // Run and add a bunch of cells + for (let i = 0; i < 20; i++) { + await addCellAndRun(`print ${i}`, notebookEditor.notebook); + } + + // Verify visible range has the last cell + assert.strictEqual(notebookEditor.visibleRanges[notebookEditor.visibleRanges.length - 1].end, notebookEditor.notebook.cellCount, `Last cell is not visible`); + + }); +}); 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 462e3113698..33cad6edb97 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -18,7 +18,7 @@ async function openRandomNotebookDocument() { return vscode.workspace.openNotebookDocument(uri); } -async function saveAllFilesAndCloseAll() { +export async function saveAllFilesAndCloseAll() { await saveAllEditors(); await closeAllEditors(); } @@ -29,14 +29,20 @@ async function withEvent(event: vscode.Event, callback: (e: Promise) => } -class Kernel { +function sleep(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +export class Kernel { readonly controller: vscode.NotebookController; readonly associatedNotebooks = new Set(); - constructor(id: string, label: string) { - this.controller = vscode.notebooks.createNotebookController(id, 'notebookCoreTest', label); + constructor(id: string, label: string, viewType: string = 'notebookCoreTest') { + this.controller = vscode.notebooks.createNotebookController(id, viewType, label); this.controller.executeHandler = this._execute.bind(this); this.controller.supportsExecutionOrder = true; this.controller.supportedLanguages = ['typescript', 'javascript']; @@ -59,8 +65,9 @@ class Kernel { // create a single output with exec order 1 and output is plain/text // of either the cell itself or (iff empty) the cell's document's uri const task = this.controller.createNotebookCellExecution(cell); - task.start(); + task.start(Date.now()); task.executionOrder = 1; + await sleep(10); // Force to be take some time await task.replaceOutput([new vscode.NotebookCellOutput([ vscode.NotebookCellOutputItem.text(cell.document.getText() || cell.document.uri.toString(), 'text/plain') ])]); diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.html b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.html index 13fd84fbd06..be1437797f8 100644 --- a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.html +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.html @@ -39,4 +39,4 @@ You signed out in another tab or window. Reload to refresh your session. - \ No newline at end of file + diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 8792cd4e6fb..bd3f3586db0 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -15,9 +15,7 @@ "notebookControllerKind", "notebookDebugOptions", "notebookDeprecated", - "notebookEditor", "notebookEditorDecorationType", - "notebookEditorEdit", "notebookLiveShare", "notebookMessaging", "notebookMime" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 97766b7943b..4ab30e237c6 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -10,17 +10,17 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" -coffee-script@^1.10.0: +coffeescript@1.12.7: version "1.12.7" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" - integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== + resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.12.7.tgz#e57ee4c4867cf7f606bfc4a0f2d550c0981ddd27" + integrity sha512-pLXHFxQMPklVoEekowk8b3erNynC+DVJzChxS/LCBBgR6/8AJkHivkm//zbowcfc7BTCAjryuhx6gPqPRfsFoA== -cson-parser@^1.3.3: - version "1.3.5" - resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" - integrity sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ= +cson-parser@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-4.0.9.tgz#eef0cf77edd057f97861ef800300c8239224eedb" + integrity sha512-I79SAcCYquWnEfXYj8hBqOOWKj6eH6zX1hhX3yqmS4K3bYp7jME3UFpHPzu3rUew0oyfc0s8T6IlWGXRAheHag== dependencies: - coffee-script "^1.10.0" + coffeescript "1.12.7" esbuild@^0.11.12: version "0.11.23" @@ -42,15 +42,15 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -typescript@4.7.1-rc: - version "4.7.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.1-rc.tgz#23a0517d36c56de887b4457f29e2d265647bbd7c" - integrity sha512-EQd2NVelDe6ZVc2sO1CSpuSs+RHzY8c2n/kTNQAHw4um/eAXY+ZY4IKoUpNK0wO6C5hN+XcUXR7yqT8VbwwNIQ== +typescript@4.7: + version "4.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" + integrity sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A== -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== +vscode-grammar-updater@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vscode-grammar-updater/-/vscode-grammar-updater-1.1.0.tgz#030eacd8b8ba8f3f2fe43c9032601f839ba811c4" + integrity sha512-rWcJXyEFK27Mh9bxfBTLaul0KiGQk0GMXj2qTDH9cy3UZVx5MrF035B03os1w4oIXwl/QDhdLnsBK0j2SNiL1A== dependencies: - cson-parser "^1.3.3" + cson-parser "^4.0.9" fast-plist "0.1.2" diff --git a/package.json b/package.json index acc64a5ce02..8dd515abb6e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.68.0", - "distro": "5e85c2493e771e21081d8f4188922225406ed06b", + "distro": "ee299d1d60e44af0115a22f8d7794f5fd185d77c", "author": { "name": "Microsoft Corporation" }, @@ -78,18 +78,19 @@ "native-watchdog": "1.4.0", "node-pty": "0.11.0-beta11", "spdlog": "^0.13.0", - "tas-client-umd": "0.1.5", + "tas-client-umd": "0.1.6", "v8-inspect-profiler": "^0.1.0", "vscode-oniguruma": "1.6.1", + "vscode-policy-watcher": "^1.1.0", "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.41", - "xterm-addon-search": "0.9.0-beta.35", + "xterm": "4.19.0-beta.56", + "xterm-addon-search": "0.9.0-beta.39", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.33", - "xterm-headless": "4.19.0-beta.41", + "xterm-addon-webgl": "0.12.0-beta.36", + "xterm-headless": "4.19.0-beta.56", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -135,7 +136,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "17.4.3", + "electron": "17.4.4", "eslint": "8.7.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^19.1.0", @@ -199,7 +200,7 @@ "style-loader": "^1.0.0", "ts-loader": "^9.2.7", "tsec": "0.1.4", - "typescript": "^4.8.0-dev.20220511", + "typescript": "^4.8.0-dev.20220518", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/product.json b/product.json index e80c93598d5..ca0f29df1da 100644 --- a/product.json +++ b/product.json @@ -27,7 +27,7 @@ "licenseFileName": "LICENSE.txt", "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", "urlProtocol": "code-oss", - "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/181b43c0e2949e36ecb623d8cc6de29d4fa2bae8/out/vs/workbench/contrib/webview/browser/pre/", + "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/3c8520fab514b9f56070214496b26ff68d1b1cb5/out/vs/workbench/contrib/webview/browser/pre/", "builtInExtensions": [ { "name": "ms-vscode.js-debug-companion", @@ -61,7 +61,7 @@ }, { "name": "ms-vscode.vscode-js-profile-table", - "version": "1.0.1", + "version": "1.0.2", "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", "metadata": { "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", diff --git a/remote/package.json b/remote/package.json index 2d7ace1fd91..68b429a5a3c 100644 --- a/remote/package.json +++ b/remote/package.json @@ -19,17 +19,17 @@ "native-watchdog": "1.4.0", "node-pty": "0.11.0-beta11", "spdlog": "^0.13.0", - "tas-client-umd": "0.1.5", + "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.41", - "xterm-addon-search": "0.9.0-beta.35", + "xterm": "4.19.0-beta.56", + "xterm-addon-search": "0.9.0-beta.39", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.33", - "xterm-headless": "4.19.0-beta.41", + "xterm-addon-webgl": "0.12.0-beta.36", + "xterm-headless": "4.19.0-beta.56", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 4a477cd72d7..87295597259 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -7,12 +7,12 @@ "@vscode/iconv-lite-umd": "0.7.0", "@vscode/vscode-languagedetection": "1.0.21", "jschardet": "3.0.0", - "tas-client-umd": "0.1.5", + "tas-client-umd": "0.1.6", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.41", - "xterm-addon-search": "0.9.0-beta.35", + "xterm": "4.19.0-beta.56", + "xterm-addon-search": "0.9.0-beta.39", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.33" + "xterm-addon-webgl": "0.12.0-beta.36" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index a852b0f91e9..b82a0e7fe58 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -98,10 +98,10 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== -tas-client-umd@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.5.tgz#743c02e344afdec55a68bb9d62805e30a1ae83d4" - integrity sha512-NL9eFzYBBHfiYja6tP27084j4YbqtGEk68C5BTyTNHapsM9dizZ/RoSUGst5L1xUiw1zO1WbHf4Lir2e/wgT8g== +tas-client-umd@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.6.tgz#a0cf70a68f50d406773457630666224f0eb545a6" + integrity sha512-eOz5IK4cuNmSZI9QlqlT0FdvgfnnHDB6rjqleFaYAbzYE4RdJzYNiM28zFIXgmOVEgESvfabMFxG8WX5M4z3HA== vscode-oniguruma@1.6.1: version "1.6.1" @@ -113,22 +113,22 @@ vscode-textmate@7.0.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== -xterm-addon-search@0.9.0-beta.35: - version "0.9.0-beta.35" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.35.tgz#524ee3be855c1e8db234c6795bdb44bb6baff8fd" - integrity sha512-hTDqAhqlhBvz3dtdK1Tg5Al2U3HquSHpV1xCX+bbOmbgprAxUrSQxslUPDD69CTazzTyif3L19M08hccRyr1Ug== +xterm-addon-search@0.9.0-beta.39: + version "0.9.0-beta.39" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.39.tgz#e8376e1485ee7d763c07d1a8f1354114f65b3e3e" + integrity sha512-h45wkecgfqXXoAUqgNytAfSd6g0xNT6rZy/enVaEU0aes7QoL9pxHUKkCry8PP6hs03Slk0VxQ4AGsbSZGvK/w== xterm-addon-unicode11@0.4.0-beta.3: version "0.4.0-beta.3" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.33: - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.33.tgz#cb539db9e41f06087b692f0f42491a73bc4bd013" - integrity sha512-seOm06exR36U0/EvR/CUNGuy99RAndoyWEdXg6S16rgEZ4G2Yj9iov/QdCtc4gwq9hFzVETFPlDW+Ge8xeHIzA== +xterm-addon-webgl@0.12.0-beta.36: + version "0.12.0-beta.36" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" + integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm@4.19.0-beta.41: - version "4.19.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.41.tgz#acb6009028898e9cfac41d4aa2865f81f6f56c5f" - integrity sha512-WY1NuxF/yUVN3l0TgzQGjrGM26eOu5g0Dbfam8GCkgdK5yrsgPF0xwM7UEj8sDjp5FbxEkSm//X86IIsgzqqFw== +xterm@4.19.0-beta.56: + version "4.19.0-beta.56" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.56.tgz#a3f1021b43ac04aa0c3f7b06f1b44ad34d487879" + integrity sha512-kywKIK61oPjbloZI+jXY1zgjQm/ghOsFFMjb79IIMaWocUDDqdpo9MmGwziTVZYu4w/Air2Zfas9UWBu4/KEyA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 968449c7184..3177a2c9ae7 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -842,10 +842,10 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tas-client-umd@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.5.tgz#743c02e344afdec55a68bb9d62805e30a1ae83d4" - integrity sha512-NL9eFzYBBHfiYja6tP27084j4YbqtGEk68C5BTyTNHapsM9dizZ/RoSUGst5L1xUiw1zO1WbHf4Lir2e/wgT8g== +tas-client-umd@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.6.tgz#a0cf70a68f50d406773457630666224f0eb545a6" + integrity sha512-eOz5IK4cuNmSZI9QlqlT0FdvgfnnHDB6rjqleFaYAbzYE4RdJzYNiM28zFIXgmOVEgESvfabMFxG8WX5M4z3HA== tunnel-agent@^0.6.0: version "0.6.0" @@ -914,10 +914,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-search@0.9.0-beta.35: - version "0.9.0-beta.35" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.35.tgz#524ee3be855c1e8db234c6795bdb44bb6baff8fd" - integrity sha512-hTDqAhqlhBvz3dtdK1Tg5Al2U3HquSHpV1xCX+bbOmbgprAxUrSQxslUPDD69CTazzTyif3L19M08hccRyr1Ug== +xterm-addon-search@0.9.0-beta.39: + version "0.9.0-beta.39" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.39.tgz#e8376e1485ee7d763c07d1a8f1354114f65b3e3e" + integrity sha512-h45wkecgfqXXoAUqgNytAfSd6g0xNT6rZy/enVaEU0aes7QoL9pxHUKkCry8PP6hs03Slk0VxQ4AGsbSZGvK/w== xterm-addon-serialize@0.7.0-beta.12: version "0.7.0-beta.12" @@ -929,20 +929,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.33: - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.33.tgz#cb539db9e41f06087b692f0f42491a73bc4bd013" - integrity sha512-seOm06exR36U0/EvR/CUNGuy99RAndoyWEdXg6S16rgEZ4G2Yj9iov/QdCtc4gwq9hFzVETFPlDW+Ge8xeHIzA== +xterm-addon-webgl@0.12.0-beta.36: + version "0.12.0-beta.36" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" + integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm-headless@4.19.0-beta.41: - version "4.19.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.41.tgz#f495ff173c7952aafa0c785acf15f20942c6fdc7" - integrity sha512-j09IFsM4tBSpjgY5OQSB1llojwEGyFFxgD36MYXZtopmB8p9+0l5GFq5hYfJojGfHCNaB/RwWAexGUxBK2ABRA== +xterm-headless@4.19.0-beta.56: + version "4.19.0-beta.56" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.56.tgz#7e6bdc8d647916bf5de64a73eee6bd508d25e344" + integrity sha512-EZoR/HqZoernhFngFQp7gUPy+G0TpEJkbJ9HVZcINC3m8wuV1wZKfZ4xBhsRPfhSJ7rsPnqbC+qez5ZjxwYEIw== -xterm@4.19.0-beta.41: - version "4.19.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.41.tgz#acb6009028898e9cfac41d4aa2865f81f6f56c5f" - integrity sha512-WY1NuxF/yUVN3l0TgzQGjrGM26eOu5g0Dbfam8GCkgdK5yrsgPF0xwM7UEj8sDjp5FbxEkSm//X86IIsgzqqFw== +xterm@4.19.0-beta.56: + version "4.19.0-beta.56" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.56.tgz#a3f1021b43ac04aa0c3f7b06f1b44ad34d487879" + integrity sha512-kywKIK61oPjbloZI+jXY1zgjQm/ghOsFFMjb79IIMaWocUDDqdpo9MmGwziTVZYu4w/Air2Zfas9UWBu4/KEyA== yallist@^4.0.0: version "4.0.0" diff --git a/resources/darwin/bin/code.sh b/resources/darwin/bin/code.sh index eecdf9c68b5..8c058727071 100755 --- a/resources/darwin/bin/code.sh +++ b/resources/darwin/bin/code.sh @@ -3,6 +3,15 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. +# when run in remote terminal, use the remote cli +if [ -n "$VSCODE_IPC_HOOK_CLI" ]; then + REMOTE_CLI="$(which -a '@@APPNAME@@' | grep /remote-cli/)" + if [ -n "$REMOTE_CLI" ]; then + "$REMOTE_CLI" "$@" + exit $? + fi +fi + function app_realpath() { SOURCE=$1 while [ -h "$SOURCE" ]; do diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index bfebec1aa8e..5fe68cb4f3e 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -3,9 +3,18 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for license information. +# when run in remote terminal, use the remote cli +if [ -n "$VSCODE_IPC_HOOK_CLI" ]; then + REMOTE_CLI="$(which -a '@@APPNAME@@' | grep /remote-cli/)" + if [ -n "$REMOTE_CLI" ]; then + "$REMOTE_CLI" "$@" + exit $? + fi +fi + # test that VSCode wasn't installed inside WSL if grep -qi Microsoft /proc/version && [ -z "$DONT_PROMPT_WSL_INSTALL" ]; then - echo "To use @@PRODNAME@@ with the Windows Subsystem for Linux, please install @@PRODNAME@@ in Windows and uninstall the Linux version in WSL. You can then use the \`@@NAME@@\` command in a WSL terminal just as you would in a normal command prompt." 1>&2 + echo "To use @@PRODNAME@@ with the Windows Subsystem for Linux, please install @@PRODNAME@@ in Windows and uninstall the Linux version in WSL. You can then use the \`@@APPNAME@@\` command in a WSL terminal just as you would in a normal command prompt." 1>&2 printf "Do you want to continue anyway? [y/N] " 1>&2 read -r YN YN=$(printf '%s' "$YN" | tr '[:upper:]' '[:lower:]') @@ -44,11 +53,11 @@ else VSCODE_PATH="$(dirname "$(readlink -f "$0")")/.." else # else use the standard install location - VSCODE_PATH="/usr/share/@@NAME@@" + VSCODE_PATH="/usr/share/@@APPNAME@@" fi fi -ELECTRON="$VSCODE_PATH/@@NAME@@" +ELECTRON="$VSCODE_PATH/@@APPNAME@@" CLI="$VSCODE_PATH/resources/app/out/cli.js" ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --ms-enable-electron-run-as-node "$@" exit $? diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 0260a781d8d..8ab9e7d76dd 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -21,19 +21,19 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Run from a built: need to compile all test extensions :: because we run extension tests from their source folders :: and the build bundles extensions into .build webpacked - call yarn gulp compile-extension:vscode-api-tests^ - compile-extension:vscode-colorize-tests^ - compile-extension:markdown-language-features^ - compile-extension:typescript-language-features^ - compile-extension:vscode-custom-editor-tests^ - compile-extension:vscode-notebook-tests^ - compile-extension:emmet^ - compile-extension:css-language-features-server^ - compile-extension:html-language-features-server^ - compile-extension:json-language-features-server^ - compile-extension:git^ - compile-extension:ipynb^ - compile-extension-media + :: call yarn gulp compile-extension:vscode-api-tests^ + :: compile-extension:vscode-colorize-tests^ + :: compile-extension:markdown-language-features^ + :: compile-extension:typescript-language-features^ + :: compile-extension:vscode-custom-editor-tests^ + :: compile-extension:vscode-notebook-tests^ + :: compile-extension:emmet^ + :: compile-extension:css-language-features-server^ + :: compile-extension:html-language-features-server^ + :: compile-extension:json-language-features-server^ + :: compile-extension:git^ + :: compile-extension:ipynb^ + :: compile-extension-media :: Configuration for more verbose output set VSCODE_CLI=1 diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 8ddfa7e9eae..e381f61b40a 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -30,19 +30,19 @@ else # Run from a built: need to compile all test extensions # because we run extension tests from their source folders # and the build bundles extensions into .build webpacked - yarn gulp compile-extension:vscode-api-tests \ - compile-extension:vscode-colorize-tests \ - compile-extension:vscode-custom-editor-tests \ - compile-extension:vscode-notebook-tests \ - compile-extension:markdown-language-features \ - compile-extension:typescript-language-features \ - compile-extension:emmet \ - compile-extension:css-language-features-server \ - compile-extension:html-language-features-server \ - compile-extension:json-language-features-server \ - compile-extension:git \ - compile-extension:ipynb \ - compile-extension-media + # yarn gulp compile-extension:vscode-api-tests \ + # compile-extension:vscode-colorize-tests \ + # compile-extension:vscode-custom-editor-tests \ + # compile-extension:vscode-notebook-tests \ + # compile-extension:markdown-language-features \ + # compile-extension:typescript-language-features \ + # compile-extension:emmet \ + # compile-extension:css-language-features-server \ + # compile-extension:html-language-features-server \ + # compile-extension:json-language-features-server \ + # compile-extension:git \ + # compile-extension:ipynb \ + # compile-extension-media # Configuration for more verbose output export VSCODE_CLI=1 diff --git a/scripts/test-remote-integration.bat b/scripts/test-remote-integration.bat index 1c04b2e392c..a7d0719205f 100644 --- a/scripts/test-remote-integration.bat +++ b/scripts/test-remote-integration.bat @@ -53,10 +53,10 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: Run from a built: need to compile all test extensions :: because we run extension tests from their source folders :: and the build bundles extensions into .build webpacked - call yarn gulp compile-extension:vscode-api-tests^ - compile-extension:microsoft-authentication^ - compile-extension:github-authentication^ - compile-extension:vscode-test-resolver + :: call yarn gulp compile-extension:vscode-api-tests^ + :: compile-extension:microsoft-authentication^ + :: compile-extension:github-authentication^ + :: compile-extension:vscode-test-resolver :: Configuration for more verbose output set VSCODE_CLI=1 diff --git a/scripts/test-remote-integration.sh b/scripts/test-remote-integration.sh index e2212317d3b..7decdf3798f 100755 --- a/scripts/test-remote-integration.sh +++ b/scripts/test-remote-integration.sh @@ -47,16 +47,16 @@ else # Run from a built: need to compile all test extensions # because we run extension tests from their source folders # and the build bundles extensions into .build webpacked - yarn gulp compile-extension:vscode-api-tests \ - compile-extension:vscode-test-resolver \ - compile-extension:markdown-language-features \ - compile-extension:typescript-language-features \ - compile-extension:emmet \ - compile-extension:git \ - compile-extension:ipynb \ - compile-extension:microsoft-authentication \ - compile-extension:github-authentication \ - compile-extension-media + # yarn gulp compile-extension:vscode-api-tests \ + # compile-extension:vscode-test-resolver \ + # compile-extension:markdown-language-features \ + # compile-extension:typescript-language-features \ + # compile-extension:emmet \ + # compile-extension:git \ + # compile-extension:ipynb \ + # compile-extension:microsoft-authentication \ + # compile-extension:github-authentication \ + # compile-extension-media # Configuration for more verbose output export VSCODE_CLI=1 diff --git a/scripts/test-web-integration.bat b/scripts/test-web-integration.bat index 99bb16b7d5e..c5b89b85b36 100644 --- a/scripts/test-web-integration.bat +++ b/scripts/test-web-integration.bat @@ -25,12 +25,12 @@ if "%VSCODE_REMOTE_SERVER_PATH%"=="" ( :: Run from a built: need to compile all test extensions :: because we run extension tests from their source folders :: and the build bundles extensions into .build webpacked - call yarn gulp compile-extension:vscode-api-tests^ - compile-extension:markdown-language-features^ - compile-extension:typescript-language-features^ - compile-extension:emmet^ - compile-extension:git^ - compile-extension-media + :: call yarn gulp compile-extension:vscode-api-tests^ + :: compile-extension:markdown-language-features^ + :: compile-extension:typescript-language-features^ + :: compile-extension:emmet^ + :: compile-extension:git^ + :: compile-extension-media ) if not exist ".\test\integration\browser\out\index.js" ( diff --git a/scripts/test-web-integration.sh b/scripts/test-web-integration.sh index 8f05929fdc4..4246cdc6ac1 100755 --- a/scripts/test-web-integration.sh +++ b/scripts/test-web-integration.sh @@ -19,13 +19,13 @@ else # Run from a built: need to compile all test extensions # because we run extension tests from their source folders # and the build bundles extensions into .build webpacked - yarn gulp compile-extension:vscode-api-tests \ - compile-extension:markdown-language-features \ - compile-extension:typescript-language-features \ - compile-extension:emmet \ - compile-extension:git \ - compile-extension:ipynb \ - compile-extension-media + # yarn gulp compile-extension:vscode-api-tests \ + # compile-extension:markdown-language-features \ + # compile-extension:typescript-language-features \ + # compile-extension:emmet \ + # compile-extension:git \ + # compile-extension:ipynb \ + # compile-extension-media fi if [ ! -e 'test/integration/browser/out/index.js' ];then diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 06a95927644..c035cb6f262 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -37,6 +37,11 @@ if (process.env['VSCODE_PARENT_PID']) { terminateWhenParentTerminates(); } +// Listen for message ports +if (process.env['VSCODE_WILL_SEND_MESSAGE_PORT']) { + listenForMessagePort(); +} + // Load AMD entry point require('./bootstrap-amd').load(process.env['VSCODE_AMD_ENTRYPOINT']); @@ -264,4 +269,17 @@ function terminateWhenParentTerminates() { } } +function listenForMessagePort() { + // We need to listen for the 'port' event as soon as possible, + // otherwise we might miss the event. But we should also be + // prepared in case the event arrives late. + process.on('port', (e) => { + if (global.vscodePortsCallback) { + global.vscodePortsCallback(e.ports); + } else { + global.vscodePorts = e.ports; + } + }); +} + //#endregion diff --git a/src/buildfile.js b/src/buildfile.js index 8c30339da6e..6b49aa30083 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -38,10 +38,6 @@ exports.base = [ }, { name: 'vs/base/common/worker/simpleWorker', - }, - { - name: 'vs/platform/extensions/node/extensionHostStarterWorker', - exclude: ['vs/base/common/worker/simpleWorker'] } ]; diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index f9f0c874eb6..7d18928f6b4 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -2,7 +2,10 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "noEmit": true, - "types": ["trusted-types"], + "types": [ + "trusted-types", + "wicg-file-system-access" + ], "paths": {}, "module": "amd", "moduleResolution": "classic", diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index d30acf1fff9..d2fa1bd3ba8 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -577,6 +577,24 @@ export function getDomNodePagePosition(domNode: HTMLElement): IDomNodePagePositi }; } +/** + * Returns the effective zoom on a given element before window zoom level is applied + */ +export function getDomNodeZoomLevel(domNode: HTMLElement): number { + let testElement: HTMLElement | null = domNode; + let zoom = 1.0; + do { + const elementZoomLevel = (getComputedStyle(testElement) as any).zoom; + if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') { + zoom *= elementZoomLevel; + } + + testElement = testElement.parentElement; + } while (testElement !== null && testElement !== document.documentElement); + + return zoom; +} + export interface IStandardWindow { readonly scrollX: number; readonly scrollY: number; diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index c34f711ae9f..1aa24bde272 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -262,11 +262,16 @@ export class ContextView extends Disposable { if (DOM.isHTMLElement(anchor)) { let elementPosition = DOM.getDomNodePagePosition(anchor); + // In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element + // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. + // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5 + const zoom = DOM.getDomNodeZoomLevel(anchor); + around = { - top: elementPosition.top, - left: elementPosition.left, - width: elementPosition.width, - height: elementPosition.height + top: elementPosition.top * zoom, + left: elementPosition.left * zoom, + width: elementPosition.width * zoom, + height: elementPosition.height * zoom }; } else { around = { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 55082221467..0bfef7f8ae7 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1597,20 +1597,25 @@ export class List implements ISpliceable, IThemable, IDisposable { async focusNextPage(browserEvent?: UIEvent, filter?: (element: T) => boolean): Promise { let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight); lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1; - const lastPageElement = this.view.element(lastPageIndex); - const currentlyFocusedElement = this.getFocusedElements()[0]; + const currentlyFocusedElementIndex = this.getFocus()[0]; - if (currentlyFocusedElement !== lastPageElement) { + if (currentlyFocusedElementIndex !== lastPageIndex && (currentlyFocusedElementIndex === undefined || lastPageIndex > currentlyFocusedElementIndex)) { const lastGoodPageIndex = this.findPreviousIndex(lastPageIndex, false, filter); - if (lastGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(lastGoodPageIndex)) { + if (lastGoodPageIndex > -1 && currentlyFocusedElementIndex !== lastGoodPageIndex) { this.setFocus([lastGoodPageIndex], browserEvent); } else { this.setFocus([lastPageIndex], browserEvent); } } else { const previousScrollTop = this.view.getScrollTop(); - this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex)); + let nextpageScrollTop = previousScrollTop + this.view.renderHeight; + if (lastPageIndex > currentlyFocusedElementIndex) { + // scroll last page element to the top only if the last page element is below the focused element + nextpageScrollTop -= this.view.elementHeight(lastPageIndex); + } + + this.view.setScrollTop(nextpageScrollTop); if (this.view.getScrollTop() !== previousScrollTop) { this.setFocus([]); @@ -1632,13 +1637,12 @@ export class List implements ISpliceable, IThemable, IDisposable { firstPageIndex = this.view.indexAfter(scrollTop - 1); } - const firstPageElement = this.view.element(firstPageIndex); - const currentlyFocusedElement = this.getFocusedElements()[0]; + const currentlyFocusedElementIndex = this.getFocus()[0]; - if (currentlyFocusedElement !== firstPageElement) { + if (currentlyFocusedElementIndex !== firstPageIndex && (currentlyFocusedElementIndex === undefined || currentlyFocusedElementIndex >= firstPageIndex)) { const firstGoodPageIndex = this.findNextIndex(firstPageIndex, false, filter); - if (firstGoodPageIndex > -1 && currentlyFocusedElement !== this.view.element(firstGoodPageIndex)) { + if (firstGoodPageIndex > -1 && currentlyFocusedElementIndex !== firstGoodPageIndex) { this.setFocus([firstGoodPageIndex], browserEvent); } else { this.setFocus([firstPageIndex], browserEvent); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index e3528de5065..e4984b09654 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -90,14 +90,13 @@ export class Menu extends ActionBar { context: options.context, actionRunner: options.actionRunner, ariaLabel: options.ariaLabel, + ariaRole: 'menu', focusOnlyEnabledItems: true, triggerKeys: { keys: [KeyCode.Enter, ...(isMacintosh || isLinux ? [KeyCode.Space] : [])], keyDown: true } }); this.menuElement = menuElement; - this.actionsList.setAttribute('role', 'menu'); - this.actionsList.tabIndex = 0; this.menuDisposables = this._register(new DisposableStore()); @@ -287,11 +286,13 @@ export class Menu extends ActionBar { const fgColor = style.foregroundColor ? `${style.foregroundColor}` : ''; const bgColor = style.backgroundColor ? `${style.backgroundColor}` : ''; const border = style.borderColor ? `1px solid ${style.borderColor}` : ''; - const shadow = style.shadowColor ? `0 2px 4px ${style.shadowColor}` : ''; + const borderRadius = '5px'; + const shadow = style.shadowColor ? `0 2px 8px ${style.shadowColor}` : ''; - container.style.border = border; - this.domNode.style.color = fgColor; - this.domNode.style.backgroundColor = bgColor; + container.style.outline = border; + container.style.borderRadius = borderRadius; + container.style.color = fgColor; + container.style.backgroundColor = bgColor; container.style.boxShadow = shadow; if (this.viewItems) { @@ -691,20 +692,19 @@ class BaseMenuActionViewItem extends BaseActionViewItem { const isSelected = this.element && this.element.classList.contains('focused'); const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor; const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : undefined; - const border = isSelected && this.menuStyle.selectionBorderColor ? `thin solid ${this.menuStyle.selectionBorderColor}` : ''; + const outline = isSelected && this.menuStyle.selectionBorderColor ? `1px solid ${this.menuStyle.selectionBorderColor}` : ''; + const outlineOffset = isSelected && this.menuStyle.selectionBorderColor ? `-1px` : ''; if (this.item) { this.item.style.color = fgColor ? fgColor.toString() : ''; this.item.style.backgroundColor = bgColor ? bgColor.toString() : ''; + this.item.style.outline = outline; + this.item.style.outlineOffset = outlineOffset; } if (this.check) { this.check.style.color = fgColor ? fgColor.toString() : ''; } - - if (this.container) { - this.container.style.border = border; - } } style(style: IMenuStyles): void { @@ -1012,7 +1012,8 @@ function getMenuWidgetCSS(style: IMenuStyles, isForShadowDom: boolean): string { let result = /* css */` .monaco-menu { font-size: 13px; - + border-radius: 5px; + min-width: 160px; } ${formatRule(Codicon.menuSelection)} @@ -1087,10 +1088,9 @@ ${formatRule(Codicon.menuSubmenu)} .monaco-menu .monaco-action-bar.vertical .action-label.separator { display: block; - border-bottom: 1px solid #bbb; + border-bottom: 1px solid var(--vscode-menu-separatorBackground); padding-top: 1px; - margin-left: .8em; - margin-right: .8em; + padding: 30px; } .monaco-menu .secondary-actions .monaco-action-bar .action-label { @@ -1136,6 +1136,11 @@ ${formatRule(Codicon.menuSubmenu)} position: relative; } +.monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .keybinding, +.monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .keybinding { + opacity: unset; +} + .monaco-menu .monaco-action-bar.vertical .action-label { flex: 1 1 auto; text-decoration: none; @@ -1191,12 +1196,9 @@ ${formatRule(Codicon.menuSubmenu)} } .monaco-menu .monaco-action-bar.vertical .action-label.separator { - padding: 0.5em 0 0 0; - margin-bottom: 0.5em; width: 100%; height: 0px !important; - margin-left: .8em !important; - margin-right: .8em !important; + opacity: 1; } .monaco-menu .monaco-action-bar.vertical .action-label.separator.text { @@ -1238,17 +1240,15 @@ ${formatRule(Codicon.menuSubmenu)} outline: 0; } -.monaco-menu .monaco-action-bar.vertical .action-item { - border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ -} - - -/* High Contrast Theming */ +.hc-black .context-view.monaco-menu-container, +.hc-light .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; } +.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused, +.hc-light .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; @@ -1257,11 +1257,11 @@ ${formatRule(Codicon.menuSubmenu)} /* Vertical Action Bar Styles */ .monaco-menu .monaco-action-bar.vertical { - padding: .5em 0; + padding: .6em 0; } .monaco-menu .monaco-action-bar.vertical .action-menu-item { - height: 1.8em; + height: 2em; } .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), @@ -1277,10 +1277,12 @@ ${formatRule(Codicon.menuSubmenu)} .monaco-menu .monaco-action-bar.vertical .action-label.separator { font-size: inherit; - padding: 0.2em 0 0 0; - margin-bottom: 0.2em; + margin: 5px 0 !important; + padding: 0; + border-radius: 0; } +.linux .monaco-menu .monaco-action-bar.vertical .action-label.separator, :host-context(.linux) .monaco-menu .monaco-action-bar.vertical .action-label.separator { margin-left: 0; margin-right: 0; @@ -1291,6 +1293,7 @@ ${formatRule(Codicon.menuSubmenu)} padding: 0 1.8em; } +.linux .monaco-menu .monaco-action-bar.vertical .submenu-indicator { :host-context(.linux) .monaco-menu .monaco-action-bar.vertical .submenu-indicator { height: 100%; mask-size: 10px 10px; diff --git a/src/vs/base/browser/ui/menu/menubar.css b/src/vs/base/browser/ui/menu/menubar.css index d2a70146718..b9dd13780e9 100644 --- a/src/vs/base/browser/ui/menu/menubar.css +++ b/src/vs/base/browser/ui/menu/menubar.css @@ -9,7 +9,8 @@ display: flex; flex-shrink: 1; box-sizing: border-box; - height: 30px; + height: 100%; + padding: 4px 0; overflow: hidden; flex-wrap: wrap; } @@ -23,6 +24,7 @@ align-items: center; box-sizing: border-box; padding: 0px 8px; + border-radius: 5px; cursor: default; -webkit-app-region: no-drag; zoom: 1; diff --git a/src/vs/base/browser/ui/table/tableWidget.ts b/src/vs/base/browser/ui/table/tableWidget.ts index eb298a582c7..03f6f284785 100644 --- a/src/vs/base/browser/ui/table/tableWidget.ts +++ b/src/vs/base/browser/ui/table/tableWidget.ts @@ -341,6 +341,10 @@ export class Table implements ISpliceable, IThemable, IDisposable { return this.list.getFocusedElements(); } + getRelativeTop(index: number): number | null { + return this.list.getRelativeTop(index); + } + reveal(index: number, relativeTop?: number): void { this.list.reveal(index, relativeTop); } diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 2fe5d37c03f..f9729a18770 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -14,6 +14,7 @@ export interface ITelemetryData { } export type WorkbenchActionExecutedClassification = { + owner: 'bpasero'; id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; from: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 8f6ab7661ae..826d3558c35 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -640,12 +640,38 @@ function getActualStartIndex(array: T[], start: number): number { return start < 0 ? Math.max(start + array.length, 0) : Math.min(start, array.length); } +/** + * When comparing two values, + * a negative number indicates that the first value is less than the second, + * a positive number indicates that the first value is greater than the second, + * and zero indicates that neither is the case. +*/ +export type CompareResult = number; + +export namespace CompareResult { + export function isLessThan(result: CompareResult): boolean { + return result < 0; + } + + export function isGreaterThan(result: CompareResult): boolean { + return result > 0; + } + + export function isNeitherLessOrGreaterThan(result: CompareResult): boolean { + return result === 0; + } + + export const greaterThan = 1; + export const lessThan = -1; + export const neitherLessOrGreaterThan = 0; +} + /** * A comparator `c` defines a total order `<=` on `T` as following: * `c(a, b) <= 0` iff `a` <= `b`. * We also have `c(a, b) == 0` iff `c(b, a) == 0`. */ -export type Comparator = (a: T, b: T) => number; +export type Comparator = (a: T, b: T) => CompareResult; export function compareBy(selector: (item: TItem) => TCompareBy, comparator: Comparator): Comparator { return (a, b) => comparator(selector(a), selector(b)); @@ -706,7 +732,7 @@ export class ArrayQueue { /** * Constructs a queue that is backed by the given array. Runtime is O(1). */ - constructor(private readonly items: T[]) { } + constructor(private readonly items: readonly T[]) { } get length(): number { return this.lastIdx - this.firstIdx + 1; @@ -748,15 +774,31 @@ export class ArrayQueue { } peek(): T | undefined { + if (this.length === 0) { + return undefined; + } return this.items[this.firstIdx]; } + peekLast(): T | undefined { + if (this.length === 0) { + return undefined; + } + return this.items[this.lastIdx]; + } + dequeue(): T | undefined { const result = this.items[this.firstIdx]; this.firstIdx++; return result; } + removeLast(): T | undefined { + const result = this.items[this.lastIdx]; + this.lastIdx--; + return result; + } + takeCount(count: number): T[] { const result = this.items.slice(this.firstIdx, this.firstIdx + count); this.firstIdx += count; diff --git a/src/vs/base/common/dataTransfer.ts b/src/vs/base/common/dataTransfer.ts new file mode 100644 index 00000000000..5074f30c75e --- /dev/null +++ b/src/vs/base/common/dataTransfer.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export interface IDataTransferFile { + readonly name: string; + readonly uri?: URI; + data(): Promise; +} + +export interface IDataTransferItem { + asString(): Thenable; + asFile(): IDataTransferFile | undefined; + value: any; +} + +export function createStringDataTransferItem(stringOrPromise: string | Promise): IDataTransferItem { + return { + asString: async () => stringOrPromise, + asFile: () => undefined, + value: typeof stringOrPromise === 'string' ? stringOrPromise : undefined, + }; +} + +export function createFileDataTransferItem(fileName: string, uri: URI | undefined, data: () => Promise): IDataTransferItem { + return { + asString: async () => '', + asFile: () => ({ name: fileName, uri, data }), + value: undefined, + }; +} + +export class VSDataTransfer { + + private readonly _entries = new Map(); + + public get size(): number { + return this._entries.size; + } + + public has(mimeType: string): boolean { + return this._entries.has(this.toKey(mimeType)); + } + + public get(mimeType: string): IDataTransferItem | undefined { + return this._entries.get(this.toKey(mimeType))?.[0]; + } + + public append(mimeType: string, value: IDataTransferItem): void { + const existing = this._entries.get(mimeType); + if (existing) { + existing.push(value); + } else { + this._entries.set(this.toKey(mimeType), [value]); + } + } + + public replace(mimeType: string, value: IDataTransferItem): void { + this._entries.set(this.toKey(mimeType), [value]); + } + + public delete(mimeType: string) { + this._entries.delete(this.toKey(mimeType)); + } + + public *entries(): Iterable<[string, IDataTransferItem]> { + for (const [mine, items] of this._entries.entries()) { + for (const item of items) { + yield [mine, item]; + } + } + } + + public values(): Iterable { + return Array.from(this._entries.values()).flat(); + } + + public forEach(f: (value: IDataTransferItem, key: string) => void) { + for (const [mime, item] of this.entries()) { + f(item, mime); + } + } + + private toKey(mimeType: string): string { + return mimeType.toLowerCase(); + } +} diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index 4f397866ae4..38ca3e4e102 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -259,8 +259,8 @@ export class ErrorNoTelemetry extends Error { * Only catch this error to recover gracefully from bugs. */ export class BugIndicatingError extends Error { - constructor(message: string) { - super(message); + constructor(message?: string) { + super(message || 'An unexpected bug occurred.'); Object.setPrototypeOf(this, BugIndicatingError.prototype); // Because we know for sure only buggy code throws this, diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index da614983689..81262c2f46a 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -46,6 +46,7 @@ export interface IJSONSchema { const?: any; contains?: IJSONSchema; propertyNames?: IJSONSchema; + examples?: any[]; // schema draft 07 $comment?: string; @@ -53,7 +54,27 @@ export interface IJSONSchema { then?: IJSONSchema; else?: IJSONSchema; - // VS Code extensions + // schema 2019-09 + unevaluatedProperties?: boolean | IJSONSchema; + unevaluatedItems?: boolean | IJSONSchema; + minContains?: number; + maxContains?: number; + deprecated?: boolean; + dependentRequired?: { [prop: string]: string[] }; + dependentSchemas?: IJSONSchemaMap; + $defs?: { [name: string]: IJSONSchema }; + $anchor?: string; + $recursiveRef?: string; + $recursiveAnchor?: string; + $vocabulary?: any; + + // schema 2020-12 + prefixItems?: IJSONSchema[]; + $dynamicRef?: string; + $dynamicAnchor?: string; + + // VSCode extensions + defaultSnippets?: IJSONSchemaSnippet[]; errorMessage?: string; patternErrorMessage?: string; diff --git a/src/vs/base/common/marked/cgmanifest.json b/src/vs/base/common/marked/cgmanifest.json index 47100d82d7f..60e11b4144e 100644 --- a/src/vs/base/common/marked/cgmanifest.json +++ b/src/vs/base/common/marked/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "marked", "repositoryUrl": "https://github.com/markedjs/marked", - "commitHash": "d1b7d521c41bcf915f81f0218b0e5acd607c1b72" + "commitHash": "2002557d004139ca2208c910d9ca999829b65406" } }, "license": "MIT", - "version": "3.0.2" + "version": "4.0.16" } ], "version": 1 diff --git a/src/vs/base/common/marked/marked.js b/src/vs/base/common/marked/marked.js index 09c308378d4..b7d8fe40e5e 100644 --- a/src/vs/base/common/marked/marked.js +++ b/src/vs/base/common/marked/marked.js @@ -23,6 +23,7 @@ typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.marked = {})); })(this, (function (exports) { 'use strict'; + function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; @@ -141,6 +142,10 @@ return html; } var unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; + /** + * @param {string} html + */ + function unescape(html) { // explicitly match decimal, hex, and named HTML entities return html.replace(unescapeTest, function (_, n) { @@ -155,8 +160,13 @@ }); } var caret = /(^|[^\[])\^/g; + /** + * @param {string | RegExp} regex + * @param {string} opt + */ + function edit(regex, opt) { - regex = regex.source || regex; + regex = typeof regex === 'string' ? regex : regex.source; opt = opt || ''; var obj = { replace: function replace(name, val) { @@ -173,6 +183,12 @@ } var nonWordAndColonTest = /[^\w:]/g; var originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; + /** + * @param {boolean} sanitize + * @param {string} base + * @param {string} href + */ + function cleanUrl(sanitize, base, href) { if (sanitize) { var prot; @@ -204,6 +220,11 @@ var justDomain = /^[^:]+:\/*[^/]*$/; var protocol = /^([^:]+:)[\s\S]*$/; var domain = /^([^:]+:\/*[^/]*)[\s\S]*$/; + /** + * @param {string} base + * @param {string} href + */ + function resolveUrl(base, href) { if (!baseUrls[' ' + base]) { // we can ignore everything in base after the last slash of its path component, @@ -282,7 +303,7 @@ cells.shift(); } - if (!cells[cells.length - 1].trim()) { + if (cells.length > 0 && !cells[cells.length - 1].trim()) { cells.pop(); } @@ -300,9 +321,15 @@ } return cells; - } // Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). - // /c*$/ is vulnerable to REDOS. - // invert: Remove suffix of non-c chars instead. Default falsey. + } + /** + * Remove trailing 'c's. Equivalent to str.replace(/c*$/, ''). + * /c*$/ is vulnerable to REDOS. + * + * @param {string} str + * @param {string} c + * @param {boolean} invert Remove suffix of non-c chars instead. Default falsey. + */ function rtrim(str, c, invert) { var l = str.length; @@ -326,7 +353,7 @@ } } - return str.substr(0, l - suffLen); + return str.slice(0, l - suffLen); } function findClosingBracket(str, b) { if (str.indexOf(b[1]) === -1) { @@ -359,6 +386,11 @@ } } // copied from https://stackoverflow.com/a/5450113/806777 + /** + * @param {string} pattern + * @param {number} count + */ + function repeatString(pattern, count) { if (count < 1) { return ''; @@ -395,15 +427,15 @@ }; lexer.state.inLink = false; return token; - } else { - return { - type: 'image', - raw: raw, - href: href, - title: title, - text: escape(text) - }; } + + return { + type: 'image', + raw: raw, + href: href, + title: title, + text: escape(text) + }; } function indentCodeCompensation(raw, text) { @@ -446,11 +478,11 @@ var cap = this.rules.block.newline.exec(src); if (cap && cap[0].length > 0) { - return { - type: 'space', - raw: cap[0] - }; - } + return { + type: 'space', + raw: cap[0] + }; + } }; _proto.code = function code(src) { @@ -526,7 +558,7 @@ var cap = this.rules.block.blockquote.exec(src); if (cap) { - var text = cap[0].replace(/^ *> ?/gm, ''); + var text = cap[0].replace(/^ *>[ \t]?/gm, ''); return { type: 'blockquote', raw: cap[0], @@ -558,7 +590,7 @@ } // Get next list item - var itemRegex = new RegExp("^( {0,3}" + bull + ")((?: [^\\n]*)?(?:\\n|$))"); // Check if current bullet point can start a new List Item + var itemRegex = new RegExp("^( {0,3}" + bull + ")((?:[\t ][^\\n]*)?(?:\\n|$))"); // Check if current bullet point can start a new List Item while (src) { endEarly = false; @@ -599,31 +631,37 @@ } if (!endEarly) { - var nextBulletRegex = new RegExp("^ {0," + Math.min(3, indent - 1) + "}(?:[*+-]|\\d{1,9}[.)])"); // Check if following lines should be included in List Item + var nextBulletRegex = new RegExp("^ {0," + Math.min(3, indent - 1) + "}(?:[*+-]|\\d{1,9}[.)])((?: [^\\n]*)?(?:\\n|$))"); + var hrRegex = new RegExp("^ {0," + Math.min(3, indent - 1) + "}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)"); // Check if following lines should be included in List Item while (src) { rawLine = src.split('\n', 1)[0]; line = rawLine; // Re-align to follow commonmark nesting rules - if (this.options.pedantic) { - line = line.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); - } // End list item if found start of new bullet + if (this.options.pedantic) { + line = line.replace(/^ {1,4}(?=( {4})*[^ ])/g, ' '); + } // End list item if found start of new bullet - if (nextBulletRegex.test(line)) { - break; + if (nextBulletRegex.test(line)) { + break; + } // Horizontal rule found + + + if (hrRegex.test(src)) { + break; } - if (line.search(/[^ ]/) >= indent || !line.trim()) { + if (line.search(/[^ ]/) >= indent || !line.trim()) { // Dedent if possible - itemContents += '\n' + line.slice(indent); + itemContents += '\n' + line.slice(indent); } else if (!blankLine) { // Until blank line, item doesn't need indentation itemContents += '\n' + line; - } else { + } else { // Otherwise, improper indentation ends this item - break; - } + break; + } if (!blankLine && !line.trim()) { // Check if current line is blank @@ -757,7 +795,7 @@ }; }), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), - rows: cap[3] ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [] + rows: cap[3] && cap[3].trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [] }; if (item.header.length === item.align.length) { @@ -793,7 +831,7 @@ for (j = 0; j < l; j++) { item.header[j].tokens = []; - this.lexer.inlineTokens(item.header[j].text, item.header[j].tokens); + this.lexer.inline(item.header[j].text, item.header[j].tokens); } // cell child tokens @@ -804,7 +842,7 @@ for (k = 0; k < row.length; k++) { row[k].tokens = []; - this.lexer.inlineTokens(row[k].text, row[k].tokens); + this.lexer.inline(row[k].text, row[k].tokens); } } @@ -1195,10 +1233,10 @@ newline: /^(?: *(?:\n|$))+/, code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/, fences: /^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/, - hr: /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/, + hr: /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/, heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/, blockquote: /^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/, - list: /^( {0,3}bull)( [^\n]+?)?(?:\n|$)/, + list: /^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/, html: '^ {0,3}(?:' // optional indentation + '<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)' // (1) + '|comment[^\\n]*(\\n+|$)' // (2) @@ -1288,9 +1326,9 @@ emStrong: { lDelim: /^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/, // (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right. - // () Skip orphan delim inside strong (1) #*** (2) a***#, a*** (3) #***a, ***a (4) ***# (5) #***# (6) a***a - rDelimAst: /^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/, - rDelimUnd: /^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/ // ^- Not allowed for _ + // () Skip orphan inside strong () Consume to delim (1) #*** (2) a***#, a*** (3) #***a, ***a (4) ***# (5) #***# (6) a***a + rDelimAst: /^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[^*]+(?=[^*])|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/, + rDelimUnd: /^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/ // ^- Not allowed for _ }, code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/, @@ -1372,6 +1410,7 @@ /** * smartypants text replacement + * @param {string} text */ function smartypants(text) { @@ -1386,6 +1425,7 @@ } /** * mangle email addresses + * @param {string} text */ @@ -1476,7 +1516,7 @@ var _proto = Lexer.prototype; _proto.lex = function lex(src) { - src = src.replace(/\r\n|\r/g, '\n').replace(/\t/g, ' '); + src = src.replace(/\r\n|\r/g, '\n'); this.blockTokens(src, this.tokens); var next; @@ -1499,7 +1539,11 @@ } if (this.options.pedantic) { - src = src.replace(/^ +$/gm, ''); + src = src.replace(/\t/g, ' ').replace(/^ +$/gm, ''); + } else { + src = src.replace(/^( *)(\t+)/gm, function (_, leading, tabs) { + return leading + ' '.repeat(tabs.length); + }); } var token, lastToken, cutSrc, lastParagraphClipped; @@ -1959,23 +2003,35 @@ } return '
' + (escaped ? _code : escape(_code, true)) + '
\n'; - }; + } + /** + * @param {string} quote + */ + ; _proto.blockquote = function blockquote(quote) { - return '
\n' + quote + '
\n'; + return "
\n" + quote + "
\n"; }; _proto.html = function html(_html) { return _html; - }; + } + /** + * @param {string} text + * @param {string} level + * @param {string} raw + * @param {any} slugger + */ + ; _proto.heading = function heading(text, level, raw, slugger) { if (this.options.headerIds) { - return '' + text + '\n'; + var id = this.options.headerPrefix + slugger.slug(raw); + return "" + text + "\n"; } // ignore IDs - return '' + text + '\n'; + return "" + text + "\n"; }; _proto.hr = function hr() { @@ -1986,55 +2042,94 @@ var type = ordered ? 'ol' : 'ul', startatt = ordered && start !== 1 ? ' start="' + start + '"' : ''; return '<' + type + startatt + '>\n' + body + '\n'; - }; + } + /** + * @param {string} text + */ + ; _proto.listitem = function listitem(text) { - return '
  • ' + text + '
  • \n'; + return "
  • " + text + "
  • \n"; }; _proto.checkbox = function checkbox(checked) { return ' '; - }; + } + /** + * @param {string} text + */ + ; _proto.paragraph = function paragraph(text) { - return '

    ' + text + '

    \n'; - }; + return "

    " + text + "

    \n"; + } + /** + * @param {string} header + * @param {string} body + */ + ; _proto.table = function table(header, body) { - if (body) body = '' + body + ''; + if (body) body = "" + body + ""; return '\n' + '\n' + header + '\n' + body + '
    \n'; - }; + } + /** + * @param {string} content + */ + ; _proto.tablerow = function tablerow(content) { - return '\n' + content + '\n'; + return "\n" + content + "\n"; }; _proto.tablecell = function tablecell(content, flags) { var type = flags.header ? 'th' : 'td'; - var tag = flags.align ? '<' + type + ' align="' + flags.align + '">' : '<' + type + '>'; - return tag + content + '\n'; - } // span level renderer + var tag = flags.align ? "<" + type + " align=\"" + flags.align + "\">" : "<" + type + ">"; + return tag + content + ("\n"); + } + /** + * span level renderer + * @param {string} text + */ ; _proto.strong = function strong(text) { - return '' + text + ''; - }; + return "" + text + ""; + } + /** + * @param {string} text + */ + ; _proto.em = function em(text) { - return '' + text + ''; - }; + return "" + text + ""; + } + /** + * @param {string} text + */ + ; _proto.codespan = function codespan(text) { - return '' + text + ''; + return "" + text + ""; }; _proto.br = function br() { return this.options.xhtml ? '
    ' : '
    '; - }; + } + /** + * @param {string} text + */ + ; _proto.del = function del(text) { - return '' + text + ''; - }; + return "" + text + ""; + } + /** + * @param {string} href + * @param {string} title + * @param {string} text + */ + ; _proto.link = function link(href, title, text) { href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); @@ -2051,7 +2146,13 @@ out += '>' + text + ''; return out; - }; + } + /** + * @param {string} href + * @param {string} title + * @param {string} text + */ + ; _proto.image = function image(href, title, text) { href = cleanUrl(this.options.sanitize, this.options.baseUrl, href); @@ -2060,10 +2161,10 @@ return text; } - var out = '' + text + '' : '>'; @@ -2133,6 +2234,10 @@ function Slugger() { this.seen = {}; } + /** + * @param {string} value + */ + var _proto = Slugger.prototype; @@ -2143,6 +2248,8 @@ } /** * Finds the next safe (unique) slug to use + * @param {string} originalSlug + * @param {boolean} isDryRun */ ; @@ -2168,8 +2275,9 @@ } /** * Convert string to unique id - * @param {object} options - * @param {boolean} options.dryrun Generates the next unique slug without updating the internal accumulator. + * @param {object} [options] + * @param {boolean} [options.dryrun] Generates the next unique slug without + * updating the internal accumulator. */ ; @@ -2866,6 +2974,7 @@ }; /** * Parse Inline + * @param {string} src */ @@ -2941,6 +3050,7 @@ exports.walkTokens = walkTokens; Object.defineProperty(exports, '__esModule', { value: true }); + })); // ESM-uncomment-begin diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index c66cf41aa6c..731c77b8893 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -74,6 +74,7 @@ export interface IProductConfiguration { readonly resourceUrlTemplate: string; readonly controlUrl: string; readonly recommendationsUrl: string; + readonly nlsBaseUrl: string; }; readonly extensionTips?: { [id: string]: string }; diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index 802cc6adfe7..efab21e77bf 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -6,10 +6,11 @@ .quick-input-widget { position: absolute; width: 600px; - z-index: 4000; + z-index: 2550; padding: 0 1px 1px 1px; left: 50%; margin-left: -300px; + -webkit-app-region: no-drag; } .quick-input-titlebar { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 0dca8a2bd38..e7099f2655b 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -979,7 +979,15 @@ class QuickPick extends QuickInput implements IQuickPi if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { this.ui.inputBox.placeholder = (this.placeholder || ''); } - const ariaLabel = this.ariaLabel || this.placeholder || QuickPick.DEFAULT_ARIA_LABEL; + + let ariaLabel = this.ariaLabel; + if (!ariaLabel) { + ariaLabel = this.placeholder || QuickPick.DEFAULT_ARIA_LABEL; + // If we have a title, include it in the aria label. + if (this.title) { + ariaLabel += ` - ${this.title}`; + } + } if (this.ui.inputBox.ariaLabel !== ariaLabel) { this.ui.inputBox.ariaLabel = ariaLabel; } @@ -1233,6 +1241,7 @@ export class QuickInputController extends Disposable { const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); checkAll.type = 'checkbox'; + checkAll.setAttribute('aria-label', localize('quickInput.checkAll', "Toggle all checkboxes")); this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { const checked = checkAll.checked; list.setAllVisibleChecked(checked); diff --git a/src/vs/base/test/browser/ui/list/listWidget.test.ts b/src/vs/base/test/browser/ui/list/listWidget.test.ts new file mode 100644 index 00000000000..bb533961b2d --- /dev/null +++ b/src/vs/base/test/browser/ui/list/listWidget.test.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { range } from 'vs/base/common/arrays'; +import { timeout } from 'vs/base/common/async'; + +suite('ListWidget', function () { + test('Page up and down', async function () { + const element = document.createElement('div'); + element.style.height = '200px'; + element.style.width = '200px'; + + const delegate: IListVirtualDelegate = { + getHeight() { return 20; }, + getTemplateId() { return 'template'; } + }; + + let templatesCount = 0; + + const renderer: IListRenderer = { + templateId: 'template', + renderTemplate() { templatesCount++; }, + renderElement() { }, + disposeTemplate() { templatesCount--; } + }; + + const listWidget = new List('test', element, delegate, [renderer]); + + listWidget.layout(200); + assert.strictEqual(templatesCount, 0, 'no templates have been allocated'); + listWidget.splice(0, 0, range(100)); + listWidget.focusFirst(); + + listWidget.focusNextPage(); + assert.strictEqual(listWidget.getFocus()[0], 9, 'first page down moves focus to element at bottom'); + + // scroll to next page is async + listWidget.focusNextPage(); + await timeout(0); + assert.strictEqual(listWidget.getFocus()[0], 19, 'page down to next page'); + + listWidget.focusPreviousPage(); + assert.strictEqual(listWidget.getFocus()[0], 10, 'first page up moves focus to element at top'); + + // scroll to previous page is async + listWidget.focusPreviousPage(); + await timeout(0); + assert.strictEqual(listWidget.getFocus()[0], 0, 'page down to previous page'); + + listWidget.dispose(); + }); + + test('Page up and down with item taller than viewport #149502', async function () { + const element = document.createElement('div'); + element.style.height = '200px'; + element.style.width = '200px'; + + const delegate: IListVirtualDelegate = { + getHeight() { return 200; }, + getTemplateId() { return 'template'; } + }; + + let templatesCount = 0; + + const renderer: IListRenderer = { + templateId: 'template', + renderTemplate() { templatesCount++; }, + renderElement() { }, + disposeTemplate() { templatesCount--; } + }; + + const listWidget = new List('test', element, delegate, [renderer]); + + listWidget.layout(200); + assert.strictEqual(templatesCount, 0, 'no templates have been allocated'); + listWidget.splice(0, 0, range(100)); + listWidget.focusFirst(); + assert.strictEqual(listWidget.getFocus()[0], 0, 'initial focus is first element'); + + // scroll to next page is async + listWidget.focusNextPage(); + await timeout(0); + assert.strictEqual(listWidget.getFocus()[0], 1, 'page down to next page'); + + // scroll to previous page is async + listWidget.focusPreviousPage(); + await timeout(0); + assert.strictEqual(listWidget.getFocus()[0], 0, 'page up to next page'); + + listWidget.dispose(); + }); +}); diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 2e4eddb19b4..d8914408e4a 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -40,6 +40,19 @@ Object.keys(self.webPackagePaths).map(function (key, index) { self.webPackagePaths[key] = `${baseUrl}/node_modules/${key}/${self.webPackagePaths[key]}`; }); + + // Set up nls if the user is not using the default language (English) + const nlsConfig = {}; + const locale = navigator.language; + if (!locale.startsWith('en')) { + nlsConfig['vs/nls'] = { + availableLanguages: { + '*': locale + }, + baseUrl: '{{WORKBENCH_NLS_BASE_URL}}' + }; + } + require.config({ baseUrl: `${baseUrl}/out`, recordStats: true, @@ -48,7 +61,8 @@ return value; } }), - paths: self.webPackagePaths + paths: self.webPackagePaths, + ...nlsConfig }); + diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 3f2ade8478f..457447af38e 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -4,7 +4,8 @@ - + - + diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js deleted file mode 100644 index d2c57f4aa10..00000000000 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ /dev/null @@ -1,1133 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 isSafari = ( - navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && - navigator.userAgent && - navigator.userAgent.indexOf('CriOS') === -1 && - navigator.userAgent.indexOf('FxiOS') === -1 -); - -const isFirefox = ( - navigator.userAgent && - navigator.userAgent.indexOf('Firefox') >= 0 -); - -const searchParams = new URL(location.toString()).searchParams; -const ID = searchParams.get('id'); -const onElectron = searchParams.get('platform') === 'electron'; -const expectedWorkerVersion = parseInt(searchParams.get('swVersion')); - -/** - * Use polling to track focus of main webview and iframes within the webview - * - * @param {Object} handlers - * @param {() => void} handlers.onFocus - * @param {() => void} handlers.onBlur - */ -const trackFocus = ({ onFocus, onBlur }) => { - const interval = 250; - let isFocused = document.hasFocus(); - setInterval(() => { - const isCurrentlyFocused = document.hasFocus(); - if (isCurrentlyFocused === isFocused) { - return; - } - isFocused = isCurrentlyFocused; - if (isCurrentlyFocused) { - onFocus(); - } else { - onBlur(); - } - }, interval); -}; - -const getActiveFrame = () => { - return /** @type {HTMLIFrameElement | undefined} */ (document.getElementById('active-frame')); -}; - -const getPendingFrame = () => { - return /** @type {HTMLIFrameElement | undefined} */ (document.getElementById('pending-frame')); -}; - -/** - * @template T - * @param {T | undefined | null} obj - * @return {T} - */ -function assertIsDefined(obj) { - if (typeof obj === 'undefined' || obj === null) { - throw new Error('Found unexpected null'); - } - return obj; -} - -const vscodePostMessageFuncName = '__vscode_post_message__'; - -const defaultStyles = document.createElement('style'); -defaultStyles.id = '_defaultStyles'; -defaultStyles.textContent = ` - html { - scrollbar-color: var(--vscode-scrollbarSlider-background) var(--vscode-editor-background); - } - - body { - background-color: transparent; - color: var(--vscode-editor-foreground); - font-family: var(--vscode-font-family); - font-weight: var(--vscode-font-weight); - font-size: var(--vscode-font-size); - margin: 0; - padding: 0 20px; - } - - img { - max-width: 100%; - max-height: 100%; - } - - a, a code { - color: var(--vscode-textLink-foreground); - } - - a:hover { - color: var(--vscode-textLink-activeForeground); - } - - a:focus, - input:focus, - select:focus, - textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; - } - - code { - color: var(--vscode-textPreformat-foreground); - } - - blockquote { - background: var(--vscode-textBlockQuote-background); - border-color: var(--vscode-textBlockQuote-border); - } - - kbd { - color: var(--vscode-editor-foreground); - border-radius: 3px; - vertical-align: middle; - padding: 1px 3px; - - background-color: hsla(0,0%,50%,.17); - border: 1px solid rgba(71,71,71,.4); - border-bottom-color: rgba(88,88,88,.4); - box-shadow: inset 0 -1px 0 rgba(88,88,88,.4); - } - .vscode-light kbd { - background-color: hsla(0,0%,87%,.5); - border: 1px solid hsla(0,0%,80%,.7); - border-bottom-color: hsla(0,0%,73%,.7); - box-shadow: inset 0 -1px 0 hsla(0,0%,73%,.7); - } - - ::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - ::-webkit-scrollbar-corner { - background-color: var(--vscode-editor-background); - } - - ::-webkit-scrollbar-thumb { - background-color: var(--vscode-scrollbarSlider-background); - } - ::-webkit-scrollbar-thumb:hover { - background-color: var(--vscode-scrollbarSlider-hoverBackground); - } - ::-webkit-scrollbar-thumb:active { - background-color: var(--vscode-scrollbarSlider-activeBackground); - } - ::highlight(find-highlight) { - background-color: var(--vscode-editor-findMatchHighlightBackground); - } - ::highlight(current-find-highlight) { - background-color: var(--vscode-editor-findMatchBackground); - }`; - -/** - * @param {boolean} allowMultipleAPIAcquire - * @param {*} [state] - * @return {string} - */ -function getVsCodeApiScript(allowMultipleAPIAcquire, state) { - const encodedState = state ? encodeURIComponent(state) : undefined; - return /* js */` - globalThis.acquireVsCodeApi = (function() { - const originalPostMessage = window.parent['${vscodePostMessageFuncName}'].bind(window.parent); - const doPostMessage = (channel, data, transfer) => { - originalPostMessage(channel, data, transfer); - }; - - let acquired = false; - - let state = ${state ? `JSON.parse(decodeURIComponent("${encodedState}"))` : undefined}; - - return () => { - if (acquired && !${allowMultipleAPIAcquire}) { - throw new Error('An instance of the VS Code API has already been acquired'); - } - acquired = true; - return Object.freeze({ - postMessage: function(message, transfer) { - doPostMessage('onmessage', { message, transfer }, transfer); - }, - setState: function(newState) { - state = newState; - doPostMessage('do-update-state', JSON.stringify(newState)); - return newState; - }, - getState: function() { - return state; - } - }); - }; - })(); - delete window.parent; - delete window.top; - delete window.frameElement; - `; -} - -/** @type {Promise} */ -const workerReady = new Promise((resolve, reject) => { - if (!areServiceWorkersEnabled()) { - return reject(new Error('Service Workers are not enabled. Webviews will not work. Try disabling private/incognito mode.')); - } - - const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}&remoteAuthority=${searchParams.get('remoteAuthority') ?? ''}`; - navigator.serviceWorker.register(swPath) - .then(() => navigator.serviceWorker.ready) - .then(async registration => { - /** - * @param {MessageEvent} event - */ - const versionHandler = async (event) => { - if (event.data.channel !== 'version') { - return; - } - - navigator.serviceWorker.removeEventListener('message', versionHandler); - if (event.data.version === expectedWorkerVersion) { - return resolve(); - } else { - console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`); - console.log(`Attempting to reload service worker`); - - // If we have the wrong version, try once (and only once) to unregister and re-register - // Note that `.update` doesn't seem to work desktop electron at the moment so we use - // `unregister` and `register` here. - return registration.unregister() - .then(() => navigator.serviceWorker.register(swPath)) - .then(() => navigator.serviceWorker.ready) - .finally(() => { resolve(); }); - } - }; - navigator.serviceWorker.addEventListener('message', versionHandler); - - const postVersionMessage = (/** @type {ServiceWorker} */ controller) => { - controller.postMessage({ channel: 'version' }); - }; - - // At this point, either the service worker is ready and - // became our controller, or we need to wait for it. - // Note that navigator.serviceWorker.controller could be a - // controller from a previously loaded service worker. - const currentController = navigator.serviceWorker.controller; - if (currentController?.scriptURL.endsWith(swPath)) { - // service worker already loaded & ready to receive messages - postVersionMessage(currentController); - } else { - // either there's no controlling service worker, or it's an old one: - // wait for it to change before posting the message - const onControllerChange = () => { - navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange); - postVersionMessage(navigator.serviceWorker.controller); - }; - navigator.serviceWorker.addEventListener('controllerchange', onControllerChange); - } - }).catch(error => { - reject(new Error(`Could not register service workers: ${error}.`)); - }); -}); - -const hostMessaging = new class HostMessaging { - - constructor() { - this.channel = new MessageChannel(); - - /** @type {Map void>>} */ - this.handlers = new Map(); - - this.channel.port1.onmessage = (e) => { - const channel = e.data.channel; - const handlers = this.handlers.get(channel); - if (handlers) { - for (const handler of handlers) { - handler(e, e.data.args); - } - } else { - console.log('no handler for ', e); - } - }; - } - - /** - * @param {string} channel - * @param {any} data - * @param {any} [transfer] - */ - postMessage(channel, data, transfer) { - this.channel.port1.postMessage({ channel, data }, transfer); - } - - /** - * @param {string} channel - * @param {(event: MessageEvent, data: any) => void} handler - */ - onMessage(channel, handler) { - let handlers = this.handlers.get(channel); - if (!handlers) { - handlers = []; - this.handlers.set(channel, handlers); - } - handlers.push(handler); - } - - 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!`); - } -}(); - -const unloadMonitor = new class { - - constructor() { - this.confirmBeforeClose = 'keyboardOnly'; - this.isModifierKeyDown = false; - - hostMessaging.onMessage('set-confirm-before-close', (_e, /** @type {string} */ data) => { - this.confirmBeforeClose = data; - }); - - hostMessaging.onMessage('content', (_e, /** @type {any} */ data) => { - this.confirmBeforeClose = data.confirmBeforeClose; - }); - - window.addEventListener('beforeunload', (event) => { - if (onElectron) { - return; - } - - switch (this.confirmBeforeClose) { - case 'always': { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - case 'never': { - break; - } - case 'keyboardOnly': - default: { - if (this.isModifierKeyDown) { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - break; - } - } - }); - } - - onIframeLoaded(/** @type {HTMLIFrameElement} */ frame) { - frame.contentWindow.addEventListener('keydown', e => { - this.isModifierKeyDown = e.metaKey || e.ctrlKey || e.altKey; - }); - - frame.contentWindow.addEventListener('keyup', () => { - this.isModifierKeyDown = false; - }); - } -}; - -// state -let firstLoad = true; -/** @type {any} */ -let loadTimeout; -let styleVersion = 0; - -/** @type {Array<{ readonly message: any, transfer?: ArrayBuffer[] }>} */ -let pendingMessages = []; - -const initData = { - /** @type {number | undefined} */ - initialScrollProgress: undefined, - - /** @type {{ [key: string]: string } | undefined} */ - styles: undefined, - - /** @type {string | undefined} */ - activeTheme: undefined, - - /** @type {string | undefined} */ - themeName: undefined, - - /** @type {boolean} */ - screenReader: false, - - /** @type {boolean} */ - reduceMotion: false, -}; - -hostMessaging.onMessage('did-load-resource', (_event, data) => { - navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []); - }); -}); - -hostMessaging.onMessage('did-load-localhost', (_event, data) => { - navigator.serviceWorker.ready.then(registration => { - assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data }); - }); -}); - -navigator.serviceWorker.addEventListener('message', event => { - switch (event.data.channel) { - case 'load-resource': - case 'load-localhost': - hostMessaging.postMessage(event.data.channel, event.data); - return; - } -}); -/** - * @param {HTMLDocument?} document - * @param {HTMLElement?} body - */ -const applyStyles = (document, body) => { - if (!document) { - return; - } - - if (body) { - 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 || ''; - } - - if (initData.styles) { - const documentStyle = document.documentElement.style; - - // Remove stale properties - for (let i = documentStyle.length - 1; i >= 0; i--) { - const property = documentStyle[i]; - - // Don't remove properties that the webview might have added separately - if (property && property.startsWith('--vscode-')) { - documentStyle.removeProperty(property); - } - } - - // Re-add new properties - for (const variable of Object.keys(initData.styles)) { - documentStyle.setProperty(`--${variable}`, initData.styles[variable]); - } - } -}; - -/** - * @param {MouseEvent} event - */ -const handleInnerClick = (event) => { - if (!event?.view?.document) { - return; - } - - const baseElement = event.view.document.querySelector('base'); - - for (const pathElement of event.composedPath()) { - /** @type {any} */ - const node = pathElement; - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - if (node.getAttribute('href') === '#') { - event.view.scrollTo(0, 0); - } else if (node.hash && (node.getAttribute('href') === node.hash || (baseElement && node.href === baseElement.href + node.hash))) { - const fragment = node.hash.slice(1); - const scrollTarget = event.view.document.getElementById(fragment) ?? event.view.document.getElementById(decodeURIComponent(fragment)); - scrollTarget?.scrollIntoView(); - } else { - hostMessaging.postMessage('did-click-link', node.href.baseVal || node.href); - } - event.preventDefault(); - return; - } - } -}; - -/** - * @param {MouseEvent} event - */ -const handleAuxClick = (event) => { - // Prevent middle clicks opening a broken link in the browser - if (!event?.view?.document) { - return; - } - - if (event.button === 1) { - for (const pathElement of event.composedPath()) { - /** @type {any} */ - const node = pathElement; - if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { - event.preventDefault(); - return; - } - } - } -}; - -/** - * @param {KeyboardEvent} e - */ -const handleInnerKeydown = (e) => { - // If the keypress would trigger a browser event, such as copy or paste, - // make sure we block the browser from dispatching it. Instead VS Code - // handles these events and will dispatch a copy/paste back to the webview - // if needed - if (isUndoRedo(e) || isPrint(e) || isFindEvent(e)) { - e.preventDefault(); - } else if (isCopyPasteOrCut(e)) { - if (onElectron) { - e.preventDefault(); - } else { - return; // let the browser handle this - } - } - - hostMessaging.postMessage('did-keydown', { - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat - }); -}; -/** - * @param {KeyboardEvent} e - */ -const handleInnerUp = (e) => { - hostMessaging.postMessage('did-keyup', { - key: e.key, - keyCode: e.keyCode, - code: e.code, - shiftKey: e.shiftKey, - altKey: e.altKey, - ctrlKey: e.ctrlKey, - metaKey: e.metaKey, - repeat: e.repeat - }); -}; - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isCopyPasteOrCut(e) { - const hasMeta = e.ctrlKey || e.metaKey; - const shiftInsert = e.shiftKey && e.key.toLowerCase() === 'insert'; - return (hasMeta && ['c', 'v', 'x'].includes(e.key.toLowerCase())) || shiftInsert; -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isUndoRedo(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && ['z', 'y'].includes(e.key.toLowerCase()); -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isPrint(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && e.key.toLowerCase() === 'p'; -} - -/** - * @param {KeyboardEvent} e - * @return {boolean} - */ -function isFindEvent(e) { - const hasMeta = e.ctrlKey || e.metaKey; - return hasMeta && e.key.toLowerCase() === 'f'; -} - -let isHandlingScroll = false; - -/** - * @param {WheelEvent} event - */ -const handleWheel = (event) => { - if (isHandlingScroll) { - return; - } - - hostMessaging.postMessage('did-scroll-wheel', { - deltaMode: event.deltaMode, - deltaX: event.deltaX, - deltaY: event.deltaY, - deltaZ: event.deltaZ, - detail: event.detail, - type: event.type - }); -}; - -/** - * @param {Event} event - */ -const handleInnerScroll = (event) => { - if (isHandlingScroll) { - return; - } - - const target = /** @type {HTMLDocument | null} */ (event.target); - const currentTarget = /** @type {Window | null} */ (event.currentTarget); - if (!currentTarget || !target?.body) { - return; - } - - const progress = currentTarget.scrollY / target.body.clientHeight; - if (isNaN(progress)) { - return; - } - - isHandlingScroll = true; - window.requestAnimationFrame(() => { - try { - hostMessaging.postMessage('did-scroll', progress); - } catch (e) { - // noop - } - isHandlingScroll = false; - }); -}; - -function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { - if (e.defaultPrevented) { - // Extension code has already handled this event - return; - } - - if (!e.dataTransfer || e.shiftKey) { - return; - } - - // Only handle drags from outside editor for now - if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) { - hostMessaging.postMessage('drag-start'); - } -} - -/** - * @param {() => void} callback - */ -function onDomReady(callback) { - if (document.readyState === 'interactive' || document.readyState === 'complete') { - callback(); - } else { - document.addEventListener('DOMContentLoaded', callback); - } -} - -function areServiceWorkersEnabled() { - try { - return !!navigator.serviceWorker; - } catch (e) { - return false; - } -} - -/** - * @typedef {{ - * contents: string; - * options: { - * readonly allowScripts: boolean; - * readonly allowForms: boolean; - * readonly allowMultipleAPIAcquire: boolean; - * } - * state: any; - * cspSource: string; - * }} ContentUpdateData - */ - -/** - * @param {ContentUpdateData} data - * @return {string} - */ -function toContentHtml(data) { - const options = data.options; - const text = data.contents; - const newDocument = new DOMParser().parseFromString(text, 'text/html'); - - newDocument.querySelectorAll('a').forEach(a => { - if (!a.title) { - const href = a.getAttribute('href'); - if (typeof href === 'string') { - a.title = href; - } - } - }); - - // Set default aria role - if (!newDocument.body.hasAttribute('role')) { - newDocument.body.setAttribute('role', 'document'); - } - - // Inject default script - if (options.allowScripts) { - const defaultScript = newDocument.createElement('script'); - defaultScript.id = '_vscodeApiScript'; - defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state); - newDocument.head.prepend(defaultScript); - } - - // Inject default styles - newDocument.head.prepend(defaultStyles.cloneNode(true)); - - applyStyles(newDocument, newDocument.body); - - // Strip out unsupported http-equiv tags - for (const metaElement of Array.from(newDocument.querySelectorAll('meta'))) { - const httpEquiv = metaElement.getAttribute('http-equiv'); - if (httpEquiv && !/^(content-security-policy|default-style|content-type)$/i.test(httpEquiv)) { - console.warn(`Removing unsupported meta http-equiv: ${httpEquiv}`); - metaElement.remove(); - } - } - - // Check for CSP - const csp = newDocument.querySelector('meta[http-equiv="Content-Security-Policy"]'); - if (!csp) { - hostMessaging.postMessage('no-csp-found'); - } else { - try { - // Attempt to rewrite CSPs that hardcode old-style resource endpoint - const cspContent = csp.getAttribute('content'); - if (cspContent) { - const newCsp = cspContent.replace(/(vscode-webview-resource|vscode-resource):(?=(\s|;|$))/g, data.cspSource); - csp.setAttribute('content', newCsp); - } - } catch (e) { - console.error(`Could not rewrite csp: ${e}`); - } - } - - // set DOCTYPE for newDocument explicitly as DOMParser.parseFromString strips it off - // and DOCTYPE is needed in the iframe to ensure that the user agent stylesheet is correctly overridden - return '\n' + newDocument.documentElement.outerHTML; -} - -onDomReady(() => { - if (!document.body) { - return; - } - - hostMessaging.onMessage('styles', (_event, data) => { - ++styleVersion; - - 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) { - return; - } - - if (target.contentDocument) { - applyStyles(target.contentDocument, target.contentDocument.body); - } - }); - - // propagate focus - hostMessaging.onMessage('focus', () => { - const activeFrame = getActiveFrame(); - if (!activeFrame || !activeFrame.contentWindow) { - // Focus the top level webview instead - window.focus(); - return; - } - - if (document.activeElement === activeFrame) { - // We are already focused on the iframe (or one of its children) so no need - // to refocus. - return; - } - - activeFrame.contentWindow.focus(); - }); - - // update iframe-contents - let updateId = 0; - hostMessaging.onMessage('content', async (_event, /** @type {ContentUpdateData} */ data) => { - const currentUpdateId = ++updateId; - try { - await workerReady; - } catch (e) { - console.error(`Webview fatal error: ${e}`); - hostMessaging.postMessage('fatal-error', { message: e + '' }); - return; - } - - if (currentUpdateId !== updateId) { - return; - } - - const options = data.options; - const newDocument = toContentHtml(data); - - const initialStyleVersion = styleVersion; - - const frame = getActiveFrame(); - const wasFirstLoad = firstLoad; - // keep current scrollY around and use later - /** @type {(body: HTMLElement, window: Window) => void} */ - let setInitialScrollPosition; - if (firstLoad) { - firstLoad = false; - setInitialScrollPosition = (body, window) => { - if (typeof initData.initialScrollProgress === 'number' && !isNaN(initData.initialScrollProgress)) { - if (window.scrollY === 0) { - window.scroll(0, body.clientHeight * initData.initialScrollProgress); - } - } - }; - } else { - const scrollY = frame && frame.contentDocument && frame.contentDocument.body ? assertIsDefined(frame.contentWindow).scrollY : 0; - setInitialScrollPosition = (body, window) => { - if (window.scrollY === 0) { - window.scroll(0, scrollY); - } - }; - } - - // Clean up old pending frames and set current one as new one - const previousPendingFrame = getPendingFrame(); - if (previousPendingFrame) { - previousPendingFrame.setAttribute('id', ''); - document.body.removeChild(previousPendingFrame); - } - if (!wasFirstLoad) { - pendingMessages = []; - } - - const newFrame = document.createElement('iframe'); - newFrame.setAttribute('id', 'pending-frame'); - newFrame.setAttribute('frameborder', '0'); - - const sandboxRules = new Set(['allow-same-origin', 'allow-pointer-lock']); - if (options.allowScripts) { - sandboxRules.add('allow-scripts'); - sandboxRules.add('allow-downloads'); - } - if (options.allowForms) { - sandboxRules.add('allow-forms'); - } - newFrame.setAttribute('sandbox', Array.from(sandboxRules).join(' ')); - if (!isFirefox) { - newFrame.setAttribute('allow', options.allowScripts ? 'clipboard-read; clipboard-write;' : ''); - } - // We should just be able to use srcdoc, but I wasn't - // seeing the service worker applying properly. - // Fake load an empty on the correct origin and then write real html - // into it to get around this. - newFrame.src = `./fake.html?id=${ID}`; - - newFrame.style.cssText = 'display: block; margin: 0; overflow: hidden; position: absolute; width: 100%; height: 100%; visibility: hidden'; - document.body.appendChild(newFrame); - - /** - * @param {Document} contentDocument - */ - function onFrameLoaded(contentDocument) { - // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=978325 - setTimeout(() => { - contentDocument.open(); - contentDocument.write(newDocument); - contentDocument.close(); - hookupOnLoadHandlers(newFrame); - - if (initialStyleVersion !== styleVersion) { - applyStyles(contentDocument, contentDocument.body); - } - }, 0); - } - - if (!options.allowScripts && isSafari) { - // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired: https://bugs.webkit.org/show_bug.cgi?id=33604 - // Use polling instead. - const interval = setInterval(() => { - // If the frame is no longer mounted, loading has stopped - if (!newFrame.parentElement) { - clearInterval(interval); - return; - } - - const contentDocument = assertIsDefined(newFrame.contentDocument); - if (contentDocument.location.pathname.endsWith('/fake.html') && contentDocument.readyState !== 'loading') { - clearInterval(interval); - onFrameLoaded(contentDocument); - } - }, 10); - } else { - assertIsDefined(newFrame.contentWindow).addEventListener('DOMContentLoaded', e => { - const contentDocument = e.target ? (/** @type {HTMLDocument} */ (e.target)) : undefined; - onFrameLoaded(assertIsDefined(contentDocument)); - }); - } - - /** - * @param {Document} contentDocument - * @param {Window} contentWindow - */ - const onLoad = (contentDocument, contentWindow) => { - if (contentDocument && contentDocument.body) { - // Workaround for https://github.com/microsoft/vscode/issues/12865 - // check new scrollY and reset if necessary - setInitialScrollPosition(contentDocument.body, contentWindow); - } - - const newFrame = getPendingFrame(); - if (newFrame && newFrame.contentDocument && newFrame.contentDocument === contentDocument) { - const wasFocused = document.hasFocus(); - const oldActiveFrame = getActiveFrame(); - if (oldActiveFrame) { - document.body.removeChild(oldActiveFrame); - } - // Styles may have changed since we created the element. Make sure we re-style - if (initialStyleVersion !== styleVersion) { - applyStyles(newFrame.contentDocument, newFrame.contentDocument.body); - } - newFrame.setAttribute('id', 'active-frame'); - newFrame.style.visibility = 'visible'; - - contentWindow.addEventListener('scroll', handleInnerScroll); - contentWindow.addEventListener('wheel', handleWheel); - - if (wasFocused) { - contentWindow.focus(); - } - - pendingMessages.forEach((message) => { - contentWindow.postMessage(message.message, window.origin, message.transfer); - }); - pendingMessages = []; - } - }; - - /** - * @param {HTMLIFrameElement} newFrame - */ - function hookupOnLoadHandlers(newFrame) { - clearTimeout(loadTimeout); - loadTimeout = undefined; - loadTimeout = setTimeout(() => { - clearTimeout(loadTimeout); - loadTimeout = undefined; - onLoad(assertIsDefined(newFrame.contentDocument), assertIsDefined(newFrame.contentWindow)); - }, 200); - - const contentWindow = assertIsDefined(newFrame.contentWindow); - - contentWindow.addEventListener('load', function (e) { - const contentDocument = /** @type {Document} */ (e.target); - - if (loadTimeout) { - clearTimeout(loadTimeout); - loadTimeout = undefined; - onLoad(contentDocument, this); - } - }); - - // Bubble out various events - contentWindow.addEventListener('click', handleInnerClick); - contentWindow.addEventListener('auxclick', handleAuxClick); - contentWindow.addEventListener('keydown', handleInnerKeydown); - contentWindow.addEventListener('keyup', handleInnerUp); - contentWindow.addEventListener('contextmenu', e => { - if (e.defaultPrevented) { - // Extension code has already handled this event - return; - } - - e.preventDefault(); - hostMessaging.postMessage('did-context-menu', { - clientX: e.clientX, - clientY: e.clientY, - }); - }); - - contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); - contentWindow.addEventListener('dragover', handleInnerDragStartEvent); - - unloadMonitor.onIframeLoaded(newFrame); - } - }); - - // Forward message to the embedded iframe - hostMessaging.onMessage('message', (_event, /** @type {{message: any, transfer?: ArrayBuffer[] }} */ data) => { - const pending = getPendingFrame(); - if (!pending) { - const target = getActiveFrame(); - if (target) { - assertIsDefined(target.contentWindow).postMessage(data.message, window.origin, data.transfer); - return; - } - } - pendingMessages.push(data); - }); - - hostMessaging.onMessage('initial-scroll-position', (_event, progress) => { - initData.initialScrollProgress = progress; - }); - - hostMessaging.onMessage('execCommand', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - assertIsDefined(target.contentDocument).execCommand(data); - }); - - /** @type {string | undefined} */ - let lastFindValue = undefined; - - hostMessaging.onMessage('find', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - - if (!data.previous && lastFindValue !== data.value) { - // Reset selection so we start search at the head of the last search - const selection = target.contentWindow.getSelection(); - selection.collapse(selection.anchorNode); - } - lastFindValue = data.value; - - const didFind = (/** @type {any} */ (target.contentWindow)).find( - data.value, - /* caseSensitive*/ false, - /* backwards*/ data.previous, - /* wrapAround*/ true, - /* wholeWord */ false, - /* searchInFrames*/ false, - false); - hostMessaging.postMessage('did-find', didFind); - }); - - hostMessaging.onMessage('find-stop', (_event, data) => { - const target = getActiveFrame(); - if (!target) { - return; - } - - lastFindValue = undefined; - - if (!data.clearSelection) { - const selection = target.contentWindow.getSelection(); - for (let i = 0; i < selection.rangeCount; i++) { - selection.removeRange(selection.getRangeAt(i)); - } - } - }); - - trackFocus({ - onFocus: () => hostMessaging.postMessage('did-focus'), - onBlur: () => hostMessaging.postMessage('did-blur') - }); - - (/** @type {any} */ (window))[vscodePostMessageFuncName] = (/** @type {string} */ command, /** @type {any} */ data) => { - switch (command) { - case 'onmessage': - case 'do-update-state': - hostMessaging.postMessage(command, data); - break; - } - }; - - // Also forward events before the contents of the webview have loaded - window.addEventListener('keydown', handleInnerKeydown); - window.addEventListener('dragenter', handleInnerDragStartEvent); - window.addEventListener('dragover', handleInnerDragStartEvent); - - hostMessaging.signalReady(); -}); diff --git a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts index 9d717dd0c1c..cdff87cccd0 100644 --- a/src/vs/workbench/contrib/webview/browser/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/browser/resourceLoading.ts @@ -65,7 +65,7 @@ export async function loadLocalResource( const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime try { - const result = await fileService.readFileStream(resourceToLoad, { etag: options.ifNoneMatch }); + const result = await fileService.readFileStream(resourceToLoad, { etag: options.ifNoneMatch }, token); return new WebviewResourceResponse.StreamSuccess(result.value, result.etag, result.mtime, mime); } catch (err) { if (err instanceof FileOperationError) { diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 3054fdda9fb..e9e90223d4e 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -14,6 +14,7 @@ import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWebviewPortMapping } from 'vs/platform/webview/common/webviewPortMapping'; +import { WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webviewElement'; /** * Set when the find widget in a webview in a webview is visible. @@ -53,12 +54,7 @@ export interface IWebviewService { /** * Create a basic webview dom element. */ - createWebviewElement( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, - ): IWebviewElement; + createWebviewElement(initInfo: WebviewInitInfo): IWebviewElement; /** * Create a lazily created webview element that is overlaid on top of another element. @@ -66,12 +62,7 @@ export interface IWebviewService { * Allows us to avoid re-parenting the webview (which destroys its contents) when * moving webview around the workbench. */ - createWebviewOverlay( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, - ): IOverlayWebview; + createWebviewOverlay(initInfo: WebviewInitInfo): IOverlayWebview; } export const enum WebviewContentPurpose { @@ -160,8 +151,16 @@ export interface WebviewMessageReceivedEvent { export interface IWebview extends IDisposable { + /** + * External identifier of this webview. + */ readonly id: string; + /** + * The origin this webview itself is loaded from. May not be unique + */ + readonly origin: string; + html: string; contentOptions: WebviewContentOptions; localResourcesRoot: readonly URI[]; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 6cd87f63f28..77aaf6683f9 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -100,11 +100,34 @@ namespace WebviewState { export type State = typeof Ready | Initializing; } +export interface WebviewInitInfo { + readonly id: string; + readonly origin?: string; + + readonly options: WebviewOptions; + readonly contentOptions: WebviewContentOptions; + + readonly extension: WebviewExtensionDescription | undefined; +} + + export class WebviewElement extends Disposable implements IWebview, WebviewFindDelegate { + /** + * External identifier of this webview. + */ public readonly id: string; + /** + * The origin this webview itself is loaded from. May not be unique + */ + public readonly origin: string; + + /** + * Unique internal identifier of this webview's iframe element. + */ private readonly iframeId: string; + private readonly encodedWebviewOriginPromise: Promise; private encodedWebviewOrigin: string | undefined; @@ -153,11 +176,12 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD private _disposed = false; + + public extension: WebviewExtensionDescription | undefined; + private readonly options: WebviewOptions; + constructor( - id: string, - private readonly options: WebviewOptions, - contentOptions: WebviewContentOptions, - public extension: WebviewExtensionDescription | undefined, + initInfo: WebviewInitInfo, protected readonly webviewThemeDataProvider: WebviewThemeDataProvider, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @@ -174,13 +198,18 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD ) { super(); - this.id = id; + this.id = initInfo.id; this.iframeId = generateUuid(); - this.encodedWebviewOriginPromise = parentOriginHash(window.origin, this.iframeId).then(id => this.encodedWebviewOrigin = id); + this.origin = initInfo.origin ?? this.iframeId; + + this.encodedWebviewOriginPromise = parentOriginHash(window.origin, this.origin).then(id => this.encodedWebviewOrigin = id); + + this.options = initInfo.options; + this.extension = initInfo.extension; this.content = { html: '', - options: contentOptions, + options: initInfo.contentOptions, state: undefined }; @@ -190,7 +219,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this._tunnelService )); - this._element = this.createElement(options, contentOptions); + this._element = this.createElement(initInfo.options, initInfo.contentOptions); const subscription = this._register(addDisposableListener(window, 'message', (e: MessageEvent) => { @@ -355,14 +384,14 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD this.startBlockingIframeDragEvents(); })); - if (options.enableFindWidget) { + if (initInfo.options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.styledFindWidget(); } this.encodedWebviewOriginPromise.then(encodedWebviewOrigin => { if (!this._disposed) { - this.initElement(encodedWebviewOrigin, extension, options); + this.initElement(encodedWebviewOrigin, this.extension, this.options); } }); } @@ -463,6 +492,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD // The extensionId and purpose in the URL are used for filtering in js-debug: const params: { [key: string]: string } = { id: this.iframeId, + origin: this.origin, swVersion: String(this._expectedServiceWorkerVersion), extensionId: extension?.id.value ?? '', platform: this.platform, diff --git a/src/vs/workbench/contrib/webview/browser/webviewService.ts b/src/vs/workbench/contrib/webview/browser/webviewService.ts index ef902f5d5e8..1ca0b71be52 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewService.ts @@ -7,8 +7,8 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; -import { IWebviewService, IWebview, WebviewContentOptions, IWebviewElement, WebviewExtensionDescription, WebviewOptions, IOverlayWebview } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewElement } from 'vs/workbench/contrib/webview/browser/webviewElement'; +import { IOverlayWebview, IWebview, IWebviewElement, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewElement, WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { OverlayWebview } from './overlayWebview'; export class WebviewService extends Disposable implements IWebviewService { @@ -43,24 +43,14 @@ export class WebviewService extends Disposable implements IWebviewService { private readonly _onDidChangeActiveWebview = this._register(new Emitter()); public readonly onDidChangeActiveWebview = this._onDidChangeActiveWebview.event; - createWebviewElement( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, - ): IWebviewElement { - const webview = this._instantiationService.createInstance(WebviewElement, id, options, contentOptions, extension, this._webviewThemeDataProvider); + createWebviewElement(initInfo: WebviewInitInfo): IWebviewElement { + const webview = this._instantiationService.createInstance(WebviewElement, initInfo, this._webviewThemeDataProvider); this.registerNewWebview(webview); return webview; } - createWebviewOverlay( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, - ): IOverlayWebview { - const webview = this._instantiationService.createInstance(OverlayWebview, id, options, contentOptions, extension); + createWebviewOverlay(initInfo: WebviewInitInfo): IOverlayWebview { + const webview = this._instantiationService.createInstance(OverlayWebview, initInfo); this.registerNewWebview(webview); return webview; } diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts index 5d77ddbc1d9..692eeb591ba 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/webviewElement.ts @@ -23,8 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ITunnelService } from 'vs/platform/tunnel/common/tunnel'; import { FindInFrameOptions, IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; -import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewElement, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/webviewElement'; +import { WebviewElement, WebviewInitInfo, WebviewMessageChannels } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { WindowIgnoreMenuShortcutsManager } from 'vs/workbench/contrib/webview/electron-sandbox/windowIgnoreMenuShortcutsManager'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -44,10 +43,7 @@ export class ElectronWebviewElement extends WebviewElement { protected override get platform() { return 'electron'; } constructor( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, + initInfo: WebviewInitInfo, webviewThemeDataProvider: WebviewThemeDataProvider, @IContextMenuService contextMenuService: IContextMenuService, @ITunnelService tunnelService: ITunnelService, @@ -64,7 +60,7 @@ export class ElectronWebviewElement extends WebviewElement { @IInstantiationService instantiationService: IInstantiationService, @IAccessibilityService accessibilityService: IAccessibilityService, ) { - super(id, options, contentOptions, extension, webviewThemeDataProvider, + super(initInfo, webviewThemeDataProvider, configurationService, contextMenuService, menuService, notificationService, environmentService, fileService, logService, remoteAuthorityResolverService, telemetryService, tunnelService, instantiationService, accessibilityService); @@ -80,7 +76,7 @@ export class ElectronWebviewElement extends WebviewElement { this._webviewKeyboardHandler.didBlur(); })); - if (options.enableFindWidget) { + if (initInfo.options.enableFindWidget) { this._register(this.onDidHtmlChange((newContent) => { if (this._findStarted && this._cachedHtmlContent !== newContent) { this.stopFind(false); diff --git a/src/vs/workbench/contrib/webview/electron-sandbox/webviewService.ts b/src/vs/workbench/contrib/webview/electron-sandbox/webviewService.ts index c63c3a65675..20ec351f827 100644 --- a/src/vs/workbench/contrib/webview/electron-sandbox/webviewService.ts +++ b/src/vs/workbench/contrib/webview/electron-sandbox/webviewService.ts @@ -3,19 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWebviewElement, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { WebviewService } from 'vs/workbench/contrib/webview/browser/webviewService'; import { ElectronWebviewElement } from 'vs/workbench/contrib/webview/electron-sandbox/webviewElement'; export class ElectronWebviewService extends WebviewService { - override createWebviewElement( - id: string, - options: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, - ): IWebviewElement { - const webview = this._instantiationService.createInstance(ElectronWebviewElement, id, options, contentOptions, extension, this._webviewThemeDataProvider); + override createWebviewElement(initInfo: WebviewInitInfo): IWebviewElement { + const webview = this._instantiationService.createInstance(ElectronWebviewElement, initInfo, this._webviewThemeDataProvider); this.registerNewWebview(webview); return webview; } diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts index 2e49d30d1f4..ec4f4c9ef91 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts @@ -21,6 +21,7 @@ interface SerializedIconPath { export interface SerializedWebview { readonly id: string; + readonly origin: string | undefined; readonly viewType: string; readonly title: string; readonly options: SerializedWebviewOptions; @@ -33,6 +34,7 @@ export interface SerializedWebview { export interface DeserializedWebview { readonly id: string; + readonly origin: string | undefined; readonly viewType: string; readonly title: string; readonly webviewOptions: WebviewOptions; @@ -74,14 +76,17 @@ export class WebviewEditorInputSerializer implements IEditorSerializer { ): WebviewInput { const data = this.fromJson(JSON.parse(serializedEditorInput)); return this._webviewWorkbenchService.reviveWebview({ - id: data.id, + webviewInitInfo: { + id: data.id, + origin: data.origin, + options: data.webviewOptions, + contentOptions: data.contentOptions, + extension: data.extension, + }, viewType: data.viewType, title: data.title, iconPath: data.iconPath, state: data.state, - webviewOptions: data.webviewOptions, - contentOptions: data.contentOptions, - extension: data.extension, group: data.group }); } @@ -100,6 +105,7 @@ export class WebviewEditorInputSerializer implements IEditorSerializer { protected toJson(input: WebviewInput): SerializedWebview { return { id: input.id, + origin: input.webview.origin, viewType: input.viewType, title: input.getName(), options: { ...input.webview.options, ...input.webview.contentOptions }, diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts index 45d5a74cdf6..94ede834b02 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts @@ -15,7 +15,8 @@ import { createDecorator, IInstantiationService } from 'vs/platform/instantiatio import { GroupIdentifier } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { IOverlayWebview, IWebviewService, WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; +import { IOverlayWebview, IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webviewElement'; import { WebviewIconManager, WebviewIcons } from 'vs/workbench/contrib/webviewPanel/browser/webviewIconManager'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; @@ -34,24 +35,18 @@ export interface IWebviewWorkbenchService { readonly iconManager: WebviewIconManager; createWebview( - id: string, + webviewInitInfo: WebviewInitInfo, viewType: string, title: string, showOptions: ICreateWebViewShowOptions, - webviewOptions: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, ): WebviewInput; reviveWebview(options: { - id: string; + webviewInitInfo: WebviewInitInfo; viewType: string; title: string; iconPath: WebviewIcons | undefined; state: any; - webviewOptions: WebviewOptions; - contentOptions: WebviewContentOptions; - extension: WebviewExtensionDescription | undefined; group: number | undefined; }): WebviewInput; @@ -218,16 +213,13 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc } public createWebview( - id: string, + webviewInitInfo: WebviewInitInfo, viewType: string, title: string, showOptions: ICreateWebViewShowOptions, - webviewOptions: WebviewOptions, - contentOptions: WebviewContentOptions, - extension: WebviewExtensionDescription | undefined, ): WebviewInput { - const webview = this._webviewService.createWebviewOverlay(id, webviewOptions, contentOptions, extension); - const webviewInput = this._instantiationService.createInstance(WebviewInput, id, viewType, title, webview, this.iconManager); + const webview = this._webviewService.createWebviewOverlay(webviewInitInfo); + const webviewInput = this._instantiationService.createInstance(WebviewInput, webviewInitInfo.id, viewType, title, webview, this.iconManager); this._editorService.openEditor(webviewInput, { pinned: true, preserveFocus: showOptions.preserveFocus, @@ -268,20 +260,17 @@ export class WebviewEditorService extends Disposable implements IWebviewWorkbenc } public reviveWebview(options: { - id: string; + webviewInitInfo: WebviewInitInfo; viewType: string; title: string; iconPath: WebviewIcons | undefined; state: any; - webviewOptions: WebviewOptions; - contentOptions: WebviewContentOptions; - extension: WebviewExtensionDescription | undefined; group: number | undefined; }): WebviewInput { - const webview = this._webviewService.createWebviewOverlay(options.id, options.webviewOptions, options.contentOptions, options.extension); + const webview = this._webviewService.createWebviewOverlay(options.webviewInitInfo); webview.state = options.state; - const webviewInput = this._instantiationService.createInstance(LazilyResolvedWebviewEditorInput, options.id, options.viewType, options.title, webview); + const webviewInput = this._instantiationService.createInstance(LazilyResolvedWebviewEditorInput, options.webviewInitInfo.id, options.viewType, options.title, webview); webviewInput.iconPath = options.iconPath; if (typeof options.group === 'number') { diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index b09e7f519cf..9a83c89d2b2 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -167,12 +167,12 @@ export class WebviewViewPane extends ViewPane { this._activated = true; const webviewId = generateUuid(); - const webview = this.webviewService.createWebviewOverlay( - webviewId, - { purpose: WebviewContentPurpose.WebviewView }, - {}, - this.extensionId ? { id: this.extensionId } : undefined - ); + const webview = this.webviewService.createWebviewOverlay({ + id: webviewId, + options: { purpose: WebviewContentPurpose.WebviewView }, + contentOptions: {}, + extension: this.extensionId ? { id: this.extensionId } : undefined + }); webview.state = this.viewState[storageKeys.webviewState]; this._webview.value = webview; @@ -283,8 +283,8 @@ export class WebviewViewPane extends ViewPane { } if (this._rootContainer) { - const clip = computeClippingRect(this._container, this._rootContainer); - webviewEntry.container.style.clip = `rect(${clip.top}px, ${clip.right}px, ${clip.bottom}px, ${clip.left}px)`; + const { top, left, right, bottom } = computeClippingRect(this._container, this._rootContainer); + webviewEntry.container.style.clipPath = `polygon(${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px)`; } } diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 977b500b88d..5fac78668bf 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -529,7 +529,7 @@ export class GettingStartedPage extends EditorPane { this.stepsContent.classList.remove('markdown'); const media = stepToExpand.media; - const webview = this.stepDisposables.add(this.webviewService.createWebviewElement(this.webviewID, {}, {}, undefined)); + const webview = this.stepDisposables.add(this.webviewService.createWebviewElement({ id: this.webviewID, options: {}, contentOptions: {}, extension: undefined })); webview.mountTo(this.stepMediaComponent); webview.html = await this.detailsRenderer.renderSVG(media.path); @@ -570,7 +570,7 @@ export class GettingStartedPage extends EditorPane { const media = stepToExpand.media; - const webview = this.stepDisposables.add(this.webviewService.createWebviewElement(this.webviewID, {}, { localResourceRoots: [media.root], allowScripts: true }, undefined)); + const webview = this.stepDisposables.add(this.webviewService.createWebviewElement({ id: this.webviewID, options: {}, contentOptions: { localResourceRoots: [media.root], allowScripts: true }, extension: undefined })); webview.mountTo(this.stepMediaComponent); const rawHTML = await this.detailsRenderer.renderMarkdown(media.path, media.base); diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 98b95174864..88955896dce 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -50,6 +50,8 @@ import { isCI, isMacintosh } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/workbench/services/files/electron-sandbox/diskFileSystemProvider'; import { FileUserDataProvider } from 'vs/platform/userData/common/fileUserDataProvider'; +import { PolicyChannelClient } from 'vs/platform/policy/common/policyIpc'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; export class DesktopMain extends Disposable { @@ -155,6 +157,10 @@ export class DesktopMain extends Disposable { const mainProcessService = this._register(new ElectronIPCMainProcessService(this.configuration.windowId)); serviceCollection.set(IMainProcessService, mainProcessService); + // Policies + const policyService = this.configuration.policiesData ? new PolicyChannelClient(this.configuration.policiesData, mainProcessService.getChannel('policy')) : new NullPolicyService(); + serviceCollection.set(IPolicyService, policyService); + // Product const productService: IProductService = { _serviceBrand: undefined, ...product }; serviceCollection.set(IProductService, productService); @@ -247,7 +253,7 @@ export class DesktopMain extends Disposable { const payload = this.resolveWorkspaceInitializationPayload(environmentService); const [configurationService, storageService] = await Promise.all([ - this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService).then(service => { + this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, uriIdentityService, logService, policyService).then(service => { // Workspace serviceCollection.set(IWorkspaceContextService, service); @@ -325,9 +331,17 @@ export class DesktopMain extends Disposable { return workspaceInitializationPayload; } - private async createWorkspaceService(payload: IAnyWorkspaceIdentifier, environmentService: INativeWorkbenchEnvironmentService, fileService: FileService, remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService): Promise { + private async createWorkspaceService( + payload: IAnyWorkspaceIdentifier, + environmentService: INativeWorkbenchEnvironmentService, + fileService: FileService, + remoteAgentService: IRemoteAgentService, + uriIdentityService: IUriIdentityService, + logService: ILogService, + policyService: IPolicyService + ): Promise { 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); + const workspaceService = new WorkspaceService({ remoteAuthority: environmentService.remoteAuthority, configurationCache }, environmentService, fileService, remoteAgentService, uriIdentityService, logService, policyService); try { await workspaceService.initialize(payload); diff --git a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts index af9a08af7c8..eb818e7fc16 100644 --- a/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-sandbox/accessibilityService.ts @@ -22,6 +22,7 @@ interface AccessibilityMetrics { enabled: boolean; } type AccessibilityMetricsClassification = { + owner: 'isidorn'; enabled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 7e06c120f95..224ce218c0f 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -171,6 +171,12 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.NotebookToolbar, description: localize('notebook.toolbar', "The contributed notebook toolbar menu") }, + { + key: 'notebook/kernelSource', + id: MenuId.NotebookKernelSource, + description: localize('notebook.kernelSource', "The contributed notebook kernel sources menu"), + proposed: 'notebookKernelSource' + }, { key: 'notebook/cell/title', id: MenuId.NotebookCellTitle, @@ -191,13 +197,11 @@ const apiMenus: IAPIMenu[] = [ key: 'interactive/toolbar', id: MenuId.InteractiveToolbar, description: localize('interactive.toolbar', "The contributed interactive toolbar menu"), - proposed: 'notebookEditor' }, { key: 'interactive/cell/title', id: MenuId.InteractiveCellTitle, description: localize('interactive.cell.title', "The contributed interactive cell title menu"), - proposed: 'notebookEditor' }, { key: 'testing/item/context', @@ -250,8 +254,14 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.InlineCompletionsActions, description: localize('inlineCompletions.actions', "The actions shown when hovering on an inline completion"), supportsSubmenus: false, - proposed: 'inlineCompletions' + proposed: 'inlineCompletionsAdditions' }, + { + key: 'merge/toolbar', + id: MenuId.MergeToolbar, + description: localize('merge.toolbar', "The prominent botton in the merge editor"), + proposed: 'contribMergeEditorToolbar' + } ]; namespace schema { diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index 2bd9b092c9a..39ed9b5e64c 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -9,7 +9,7 @@ import * as errors from 'vs/base/common/errors'; import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult, FileOperation, FileOperationEvent } from 'vs/platform/files/common/files'; -import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, DefaultConfigurationModel, UserSettings } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel, ConfigurationModelParser, ConfigurationParseOptions, UserSettings } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -26,8 +26,9 @@ import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { isObject } from 'vs/base/common/types'; +import { DefaultConfiguration as BaseDefaultConfiguration } from 'vs/platform/configuration/common/configurations'; -export class DefaultConfiguration extends Disposable { +export class DefaultConfiguration extends BaseDefaultConfiguration { static readonly DEFAULT_OVERRIDES_CACHE_EXISTS_KEY = 'DefaultOverridesCacheExists'; @@ -35,9 +36,6 @@ export class DefaultConfiguration extends Disposable { private cachedConfigurationDefaultsOverrides: IStringDictionary = {}; private readonly cacheKey: ConfigurationKey = { type: 'defaults', key: 'configurationDefaultsOverrides' }; - private readonly _onDidChangeConfiguration = this._register(new Emitter<{ defaults: ConfigurationModel; properties: string[] }>()); - readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; - private updateCache: boolean = false; constructor( @@ -50,27 +48,20 @@ export class DefaultConfiguration extends Disposable { } } - private _configurationModel: ConfigurationModel | undefined; - get configurationModel(): ConfigurationModel { - if (!this._configurationModel) { - this._configurationModel = new DefaultConfigurationModel(this.cachedConfigurationDefaultsOverrides); - } - return this._configurationModel; + protected override getConfigurationDefaultOverrides(): IStringDictionary { + return this.cachedConfigurationDefaultsOverrides; } - async initialize(): Promise { + override async initialize(): Promise { await this.initializeCachedConfigurationDefaultsOverrides(); - this._configurationModel = undefined; - this._register(this.configurationRegistry.onDidUpdateConfiguration(({ properties, defaultsOverrides }) => this.onDidUpdateConfiguration(properties, defaultsOverrides))); - return this.configurationModel; + return super.initialize(); } - reload(): ConfigurationModel { + override reload(): ConfigurationModel { this.updateCache = true; this.cachedConfigurationDefaultsOverrides = {}; - this._configurationModel = undefined; this.updateCachedConfigurationDefaultsOverrides(); - return this.configurationModel; + return super.reload(); } private initiaizeCachedConfigurationDefaultsOverridesPromise: Promise | undefined; @@ -92,9 +83,8 @@ export class DefaultConfiguration extends Disposable { return this.initiaizeCachedConfigurationDefaultsOverridesPromise; } - private onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { - this._configurationModel = undefined; - this._onDidChangeConfiguration.fire({ defaults: this.configurationModel, properties }); + protected override onDidUpdateConfiguration(properties: string[], defaultsOverrides?: boolean): void { + super.onDidUpdateConfiguration(properties, defaultsOverrides); if (defaultsOverrides) { this.updateCachedConfigurationDefaultsOverrides(); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 45af17cc175..d32d7e978bb 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -13,6 +13,7 @@ import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/plat import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder, isWorkspaceFolder, IWorkspaceFoldersWillChangeEvent, IEmptyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IWorkspaceIdentifier, IAnyWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; import { ConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString, IConfigurationUpdateOverrides, isConfigurationUpdateOverrides, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IPolicyConfiguration, NullPolicyConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, RestrictedSettings } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -39,6 +40,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkbenchAssignmentService } from 'vs/workbench/services/assignment/common/assignmentService'; import { isUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; +import { IPolicyService, NullPolicyService } from 'vs/platform/policy/common/policy'; class Workspace extends BaseWorkspace { initialized: boolean = false; @@ -54,7 +56,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private readonly configurationCache: IConfigurationCache; private _configuration: Configuration; private initialized: boolean = false; - private defaultConfiguration: DefaultConfiguration; + private readonly defaultConfiguration: DefaultConfiguration; + private readonly policyConfiguration: IPolicyConfiguration; private localUserConfiguration: UserConfiguration; private remoteUserConfiguration: RemoteUserConfiguration | null = null; private workspaceConfiguration: WorkspaceConfiguration; @@ -102,6 +105,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat remoteAgentService: IRemoteAgentService, uriIdentityService: IUriIdentityService, logService: ILogService, + policyService: IPolicyService ) { super(); @@ -109,12 +113,13 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat this.initRemoteUserConfigurationBarrier = new Barrier(); this.completeWorkspaceBarrier = new Barrier(); - this.defaultConfiguration = new DefaultConfiguration(configurationCache, environmentService); + this.defaultConfiguration = this._register(new DefaultConfiguration(configurationCache, environmentService)); + this.policyConfiguration = policyService instanceof NullPolicyService ? new NullPolicyConfiguration() : this._register(new PolicyConfiguration(this.defaultConfiguration, policyService, logService)); this.configurationCache = configurationCache; this.fileService = fileService; this.uriIdentityService = uriIdentityService; this.logService = logService; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), this.workspace); this.cachedFolderConfigs = new ResourceMap(); this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService, uriIdentityService, logService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); @@ -138,6 +143,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat })); this._register(this.defaultConfiguration.onDidChangeConfiguration(({ properties, defaults }) => this.onDefaultConfigurationChanged(defaults, properties))); + this._register(this.policyConfiguration.onDidChangeConfiguration(configurationModel => this.onPolicyConfigurationChanged(configurationModel))); this.workspaceEditingQueue = new Queue(); } @@ -338,6 +344,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat async reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise { if (target === undefined) { + this.reloadDefaultConfiguration(); const { local, remote } = await this.reloadUserConfiguration(); await this.reloadWorkspaceConfiguration(); await this.loadConfiguration(local, remote); @@ -351,7 +358,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat switch (target) { case ConfigurationTarget.DEFAULT: - await this.reloadDefaultConfiguration(); + this.reloadDefaultConfiguration(); return; case ConfigurationTarget.USER: { @@ -572,12 +579,18 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat private async initializeConfiguration(): Promise { await this.defaultConfiguration.initialize(); - mark('code/willInitUserConfiguration'); - const { local, remote } = await this.initializeUserConfiguration(); - mark('code/didInitUserConfiguration'); + const [, user] = await Promise.all([ + this.policyConfiguration.initialize(), + (async () => { + mark('code/willInitUserConfiguration'); + const result = await this.initializeUserConfiguration(); + mark('code/didInitUserConfiguration'); + return result; + })() + ]); mark('code/willInitWorkspaceConfiguration'); - await this.loadConfiguration(local, remote); + await this.loadConfiguration(user.local, user.remote); mark('code/didInitWorkspaceConfiguration'); } @@ -586,7 +599,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat return { local, remote }; } - private async reloadDefaultConfiguration(): Promise { + private reloadDefaultConfiguration(): void { this.onDefaultConfigurationChanged(this.defaultConfiguration.reload()); } @@ -640,7 +653,7 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration)); const currentConfiguration = this._configuration; - this._configuration = new Configuration(this.defaultConfiguration.configurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); + this._configuration = new Configuration(this.defaultConfiguration.configurationModel, this.policyConfiguration.configurationModel, userConfigurationModel, remoteUserConfigurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap(), this.workspace); if (this.initialized) { const change = this._configuration.compare(currentConfiguration); @@ -692,6 +705,12 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } + private onPolicyConfigurationChanged(policyConfiguration: ConfigurationModel): void { + const previous = { data: this._configuration.toData(), workspace: this.workspace }; + const change = this._configuration.compareAndUpdatePolicyConfiguration(policyConfiguration); + this.triggerConfigurationChange(change, previous, ConfigurationTarget.DEFAULT); + } + private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 3e4e805c231..bf0d5f3adde 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -92,6 +92,11 @@ export const enum ConfigurationEditingErrorCode { */ ERROR_INVALID_CONFIGURATION, + /** + * Error when trying to write a policy configuration + */ + ERROR_POLICY_CONFIGURATION, + /** * Internal Error. */ @@ -359,6 +364,7 @@ export class ConfigurationEditingService { switch (error) { // API constraints + case ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION: return nls.localize('errorPolicyConfiguration', "Unable to write {0} because it is configured in system policy.", operation.key); case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); case ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_MACHINE: return nls.localize('errorInvalidWorkspaceConfigurationMachine', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", operation.key); @@ -492,6 +498,10 @@ export class ConfigurationEditingService { private async validate(target: EditableConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationUpdateOverrides): Promise { + if (this.configurationService.inspect(operation.key).policyValue !== undefined) { + throw this.toConfigurationEditingError(ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION, target, operation); + } + const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); const configurationScope = configurationProperties[operation.key]?.scope; diff --git a/src/vs/workbench/services/configuration/common/configurationModels.ts b/src/vs/workbench/services/configuration/common/configurationModels.ts index 95a1d40ad77..807e96399df 100644 --- a/src/vs/workbench/services/configuration/common/configurationModels.ts +++ b/src/vs/workbench/services/configuration/common/configurationModels.ts @@ -98,6 +98,7 @@ export class Configuration extends BaseConfiguration { constructor( defaults: ConfigurationModel, + policy: ConfigurationModel, localUser: ConfigurationModel, remoteUser: ConfigurationModel, workspaceConfiguration: ConfigurationModel, @@ -105,7 +106,7 @@ export class Configuration extends BaseConfiguration { memoryConfiguration: ConfigurationModel, memoryConfigurationByResource: ResourceMap, private readonly _workspace?: Workspace) { - super(defaults, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); + super(defaults, policy, localUser, remoteUser, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource); } override getValue(key: string | undefined, overrides: IConfigurationOverrides = {}): any { 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 7d7e3cb1267..995442cd8b8 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts @@ -6,6 +6,7 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; import * as json from 'vs/base/common/json'; +import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -37,8 +38,11 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; -import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { hash } from 'vs/base/common/hash'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -52,7 +56,7 @@ export class ConfigurationCache implements IConfigurationCache { suite('ConfigurationEditingService', () => { let instantiationService: TestInstantiationService; - let environmentService: BrowserWorkbenchEnvironmentService; + let environmentService: IWorkbenchEnvironmentService; let fileService: IFileService; let workspaceService: WorkspaceService; let testObject: ConfigurationEditingService; @@ -76,6 +80,14 @@ suite('ConfigurationEditingService', () => { 'configurationEditing.service.testSettingThree': { 'type': 'string', 'default': 'isSet' + }, + 'configurationEditing.service.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationEditing.service.policySetting', + minimumVersion: '1.0.0', + } } } }); @@ -92,12 +104,17 @@ suite('ConfigurationEditingService', () => { instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(workspaceFolder, 'policies.json'); instantiationService.stub(IEnvironmentService, environmentService); const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); 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())); + workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); + await workspaceService.initialize({ + id: hash(workspaceFolder.toString()).toString(16), + uri: workspaceFolder + }); instantiationService.stub(IWorkspaceContextService, workspaceService); await workspaceService.initialize(getSingleFolderWorkspaceIdentifier(workspaceFolder)); @@ -180,6 +197,28 @@ suite('ConfigurationEditingService', () => { assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'); }); + test('errors cases - ERROR_POLICY_CONFIGURATION', async () => { + await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(instantiationService.get(IConfigurationService).onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationEditing.service.policySetting": "policyValue" }')); + await promise; + }); + try { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + } catch (error) { + assert.strictEqual(error.code, ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION); + return; + } + assert.fail('Should fail with ERROR_POLICY_CONFIGURATION'); + }); + + test('write policy setting - when not set', async () => { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + const contents = await fileService.readFile(environmentService.settingsResource); + const parsed = json.parse(contents.value.toString()); + assert.strictEqual(parsed['configurationEditing.service.policySetting'], 'value'); + }); + test('write one setting - empty file', async () => { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }); const contents = await fileService.readFile(environmentService.settingsResource); 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 352fd8e9d7e..d36be57c247 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -44,6 +44,9 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteA import { RemoteAuthorityResolverService } from 'vs/platform/remote/browser/remoteAuthorityResolverService'; import { hash } from 'vs/base/common/hash'; import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { NullPolicyService } from 'vs/platform/policy/common/policy'; +import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; function convertToWorkspacePayload(folder: URI): ISingleFolderWorkspaceIdentifier { return { @@ -77,7 +80,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; 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(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); }); @@ -117,7 +120,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); const actual = testObject.getWorkspaceFolder(joinPath(folder, 'a')); @@ -137,7 +140,7 @@ suite('WorkspaceContextService - Folder', () => { const environmentService = TestEnvironmentService; fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); - const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService())); + const testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, new RemoteAgentService(null, environmentService, TestProductService, new RemoteAuthorityResolverService(TestProductService, undefined, undefined), new SignService(undefined), new NullLogService()), new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); await (testObject).initialize(convertToWorkspacePayload(folder)); @@ -184,7 +187,7 @@ suite('WorkspaceContextService - Workspace', () => { const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); instantiationService.stub(IRemoteAgentService, remoteAgentService); 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())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -242,7 +245,7 @@ suite('WorkspaceContextService - Workspace Editing', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); 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())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); @@ -485,7 +488,7 @@ suite('WorkspaceService - Initialization', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); 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())); + testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -669,7 +672,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -708,6 +711,14 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', restricted: true }, + 'configurationService.folder.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationService.folder.policySetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -731,10 +742,11 @@ suite('WorkspaceConfigurationService - Folder', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(folder, 'policies.json'); const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); 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())); + workspaceService = testObject = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new FilePolicyService(environmentService.policyFile, fileService, logService))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); @@ -750,7 +762,7 @@ suite('WorkspaceConfigurationService - Folder', () => { teardown(() => disposables.clear()); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); }); test('globals override defaults', async () => { @@ -956,6 +968,25 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); + test('policy value override all', async () => { + const result = await runWithFakedTimers({ useFakeTimers: true }, async () => { + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policySetting": "policyValue" }')); + return promise; + }); + assert.deepStrictEqual(result.affectedKeys, ['configurationService.folder.policySetting']); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'policyValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, 'policyValue'); + }); + + test('policy settings when policy value is not set', async () => { + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policySetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.policySetting": "workspaceValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'workspaceValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); + }); + test('reload configuration emits events after global configuraiton changes', async () => { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy(); @@ -1405,7 +1436,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); 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())); + const workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IFileService, fileService); instantiationService.stub(IWorkspaceContextService, workspaceService); @@ -2066,7 +2097,7 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { const remoteAgentService = instantiationService.stub(IRemoteAgentService, >{ getEnvironment: () => remoteEnvironmentPromise }); 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())); + testObject = disposables.add(new WorkspaceService({ configurationCache, remoteAuthority }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService(), new NullPolicyService())); instantiationService.stub(IWorkspaceContextService, testObject); instantiationService.stub(IConfigurationService, testObject); instantiationService.stub(IEnvironmentService, environmentService); diff --git a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts index 228cc13113a..7f391a11ad3 100644 --- a/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts +++ b/src/vs/workbench/services/configuration/test/common/configurationModels.test.ts @@ -155,14 +155,14 @@ suite('Workspace Configuration', () => { test('Test compare same configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); @@ -176,14 +176,14 @@ suite('Workspace Configuration', () => { test('Test compare different configurations', () => { const workspace = new Workspace('a', [new WorkspaceFolder({ index: 0, name: 'a', uri: URI.file('folder1') }), new WorkspaceFolder({ index: 1, name: 'b', uri: URI.file('folder2') }), new WorkspaceFolder({ index: 2, name: 'c', uri: URI.file('folder3') })]); - const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration1 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration1.updateDefaultConfiguration(defaultConfigurationModel); configuration1.updateLocalUserConfiguration(toConfigurationModel({ 'window.title': 'native', '[typescript]': { 'editor.insertSpaces': false } })); configuration1.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.lineNumbers': 'on' })); configuration1.updateFolderConfiguration(URI.file('folder1'), toConfigurationModel({ 'editor.fontSize': 14 })); configuration1.updateFolderConfiguration(URI.file('folder2'), toConfigurationModel({ 'editor.wordWrap': 'on' })); - const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); + const configuration2 = new Configuration(new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), workspace); configuration2.updateDefaultConfiguration(defaultConfigurationModel); configuration2.updateLocalUserConfiguration(toConfigurationModel({ 'workbench.enableTabs': true, '[typescript]': { 'editor.insertSpaces': true } })); configuration2.updateWorkspaceConfiguration(toConfigurationModel({ 'editor.fontSize': 11 })); diff --git a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts index b491ce7079e..b4c0e11cbeb 100644 --- a/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts +++ b/src/vs/workbench/services/contextmenu/electron-sandbox/contextmenuService.ts @@ -105,15 +105,7 @@ class NativeContextMenuService extends Disposable implements IContextMenuService // In areas where zoom is applied to the element or its ancestors, we need to adjust accordingly // e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level. // Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Coordinate Multiplier: 1.5 * 1.0 / 1.5 = 1.0 - let testElement: HTMLElement | null = anchor; - do { - const elementZoomLevel = (dom.getComputedStyle(testElement) as any).zoom; - if (elementZoomLevel !== null && elementZoomLevel !== undefined && elementZoomLevel !== '1') { - zoom *= elementZoomLevel; - } - - testElement = testElement.parentElement; - } while (testElement !== null && testElement !== document.documentElement); + zoom *= dom.getDomNodeZoomLevel(anchor); x = elementPosition.left; y = elementPosition.top + elementPosition.height; diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index 1fc0ccc3570..87f6e2ca052 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -16,7 +16,7 @@ import { basename } from 'vs/base/common/resources'; import { triggerDownload, triggerUpload } from 'vs/base/browser/dom'; import Severity from 'vs/base/common/severity'; import { VSBuffer } from 'vs/base/common/buffer'; -import { extractFileListData } from 'vs/workbench/browser/dnd'; +import { extractFileListData } from 'vs/platform/dnd/browser/dnd'; import { Iterable } from 'vs/base/common/iterator'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index d41839b87ba..f205de47189 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -189,7 +189,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi const webviewExternalEndpointCommit = this.payload?.get('webviewExternalEndpointCommit'); return endpoint - .replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '181b43c0e2949e36ecb623d8cc6de29d4fa2bae8') + .replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '3c8520fab514b9f56070214496b26ff68d1b1cb5') .replace('{{quality}}', (webviewExternalEndpointCommit ? 'insider' : this.productService.quality) ?? 'insider'); } diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 8438e2f3377..1124b843b01 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -140,10 +140,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten this.logService.info(`Checking additional builtin extensions: Ignoring '${extension.id}' because it is reported to be malicious.`); continue; } - if (extensionsControlManifest.unsupportedPreReleaseExtensions && extensionsControlManifest.unsupportedPreReleaseExtensions[extension.id.toLowerCase()]) { - const preReleaseExtensionId = extensionsControlManifest.unsupportedPreReleaseExtensions[extension.id.toLowerCase()].id; - this.logService.info(`Checking additional builtin extensions: '${extension.id}' is no longer supported, instead using '${preReleaseExtensionId}'`); - result.push({ id: preReleaseExtensionId, preRelease: true }); + const deprecationInfo = extensionsControlManifest.deprecated[extension.id.toLowerCase()]; + if (deprecationInfo?.extension?.autoMigrate) { + const preReleaseExtensionId = deprecationInfo.extension.id; + this.logService.info(`Checking additional builtin extensions: '${extension.id}' is deprecated, instead using '${preReleaseExtensionId}'`); + result.push({ id: preReleaseExtensionId, preRelease: !!extension.preRelease }); } else { result.push(extension); } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index cc9bcdd1045..46c26ac1795 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -394,7 +394,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench if (this.extensionManagementServerService.webExtensionManagementServer) { return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.getExtensionsControlManifest(); } - return Promise.resolve({ malicious: [] }); + return Promise.resolve({ malicious: [], deprecated: {} }); } private getServer(extension: ILocalExtension): IExtensionManagementServer | null { diff --git a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts index 5c045163b1b..f2008b07592 100644 --- a/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts +++ b/src/vs/workbench/services/extensions/browser/extensionUrlHandler.ts @@ -82,6 +82,7 @@ export interface ExtensionUrlHandlerEvent { } export interface ExtensionUrlHandlerClassification extends GDPRClassification { + owner: 'joaomoreno'; readonly extensionId: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index c562da5671f..5280e6584a2 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -11,6 +11,7 @@ export const allApiProposals = Object.freeze({ 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', + contribMergeEditorToolbar: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts', contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts', contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts', contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts', @@ -18,6 +19,7 @@ export const allApiProposals = Object.freeze({ dataTransferFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataTransferFiles.d.ts', diffCommand: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.diffCommand.d.ts', documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', + documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', @@ -26,7 +28,6 @@ export const allApiProposals = Object.freeze({ findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', 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', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', @@ -38,15 +39,16 @@ export const allApiProposals = Object.freeze({ 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', + notebookKernelSource: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookKernelSource.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', - notebookProxyController: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts', notebookWorkspaceEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.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', scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', + scmInput: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmInput.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', diff --git a/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts new file mode 100644 index 00000000000..2e4c745a9f8 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ExtensionHostKind, ExtensionRunningLocation, IExtensionHost, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { NativeLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost'; +import { ElectronExtensionService } from 'vs/workbench/services/extensions/electron-sandbox/electronExtensionService'; + +export class NativeExtensionService extends ElectronExtensionService { + protected override _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { + if (runningLocation.kind === ExtensionHostKind.LocalProcess) { + return this._instantiationService.createInstance(NativeLocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + } + return super._createExtensionHost(runningLocation, isInitialStart); + } +} + +registerSingleton(IExtensionService, NativeExtensionService); diff --git a/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts new file mode 100644 index 00000000000..d498ddd7960 --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-browser/nativeLocalProcessExtensionHost.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createServer, Server } from 'net'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; +import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { IExtensionHostProcessOptions } from 'vs/platform/extensions/common/extensionHostStarter'; +import { ILogService } from 'vs/platform/log/common/log'; +import { createMessageOfType, MessageType } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { ExtensionHostProcess, ExtHostMessagePortCommunication, IExtHostCommunication, SandboxLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost'; + +export class NativeLocalProcessExtensionHost extends SandboxLocalProcessExtensionHost { + protected override async _start(): Promise { + const canUseUtilityProcess = await this._extensionHostStarter.canUseUtilityProcess(); + if (canUseUtilityProcess && process.env['VSCODE_USE_UTILITY_PROCESS']) { + const communication = this._toDispose.add(new ExtHostMessagePortCommunication(this._logService)); + return this._startWithCommunication(communication); + } else { + const communication = this._toDispose.add(new ExtHostNamedPipeCommunication(this._logService)); + return this._startWithCommunication(communication); + } + } +} + +interface INamedPipePreparedData { + pipeName: string; + namedPipeServer: Server; +} + +class ExtHostNamedPipeCommunication extends Disposable implements IExtHostCommunication { + + readonly useUtilityProcess = false; + + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + } + + prepare(): Promise { + return new Promise<{ pipeName: string; namedPipeServer: Server }>((resolve, reject) => { + const pipeName = createRandomIPCHandle(); + + const namedPipeServer = createServer(); + namedPipeServer.on('error', reject); + namedPipeServer.listen(pipeName, () => { + if (namedPipeServer) { + namedPipeServer.removeListener('error', reject); + } + resolve({ pipeName, namedPipeServer }); + }); + this._register(toDisposable(() => { + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + })); + }); + } + + establishProtocol(prepared: INamedPipePreparedData, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise { + const { namedPipeServer, pipeName } = prepared; + + opts.env['VSCODE_IPC_HOOK_EXTHOST'] = pipeName; + + return new Promise((resolve, reject) => { + + // Wait for the extension host to connect to our named pipe + // and wrap the socket in the message passing protocol + const handle = setTimeout(() => { + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + reject('The local extension host took longer than 60s to connect.'); + }, 60 * 1000); + + namedPipeServer.on('connection', (socket) => { + + clearTimeout(handle); + if (namedPipeServer.listening) { + namedPipeServer.close(); + } + + const nodeSocket = new NodeSocket(socket, 'renderer-exthost'); + const protocol = new PersistentProtocol(nodeSocket); + + this._register(toDisposable(() => { + // Send the extension host a request to terminate itself + // (graceful termination) + protocol.send(createMessageOfType(MessageType.Terminate)); + protocol.flush(); + + socket.end(); + nodeSocket.dispose(); + protocol.dispose(); + })); + + resolve(protocol); + }); + + // Now that the named pipe listener is installed, start the ext host process + const sw = StopWatch.create(false); + extensionHostProcess.start(opts).then(() => { + const duration = sw.elapsed(); + if (platform.isCI) { + this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); + } + }, (err) => { + // Starting the ext host process resulted in an error + reject(err); + }); + + }); + } +} diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts similarity index 97% rename from src/vs/workbench/services/extensions/electron-browser/extensionService.ts rename to src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts index 388033c90a1..ea87ef0642a 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/electronExtensionService.ts @@ -3,10 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, 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, ExtensionHostCrashTracker, ExtensionRunningPreference, extensionRunningPreferenceToString, filterByRunningLocation } from 'vs/workbench/services/extensions/common/abstractExtensionService'; import * as nls from 'vs/nls'; import { runWhenIdle } from 'vs/base/common/async'; @@ -50,8 +47,9 @@ 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'; +import { ILocalProcessExtensionHostDataProvider, ILocalProcessExtensionHostInitData, SandboxLocalProcessExtensionHost } from 'vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost'; -export class ExtensionService extends AbstractExtensionService implements IExtensionService { +export abstract class ElectronExtensionService extends AbstractExtensionService implements IExtensionService { private readonly _enableLocalWebWorker: boolean; private readonly _lazyLocalWebWorker: boolean; @@ -155,7 +153,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten ])); } - private _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { + protected _createLocalExtensionHostDataProvider(isInitialStart: boolean, desiredRunningLocation: ExtensionRunningLocation): ILocalProcessExtensionHostDataProvider & IWebWorkerExtensionHostDataProvider { return { getInitData: async (): Promise => { if (isInitialStart) { @@ -193,7 +191,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } 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); + const result = ElectronExtensionService.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; } @@ -240,7 +238,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten protected _createExtensionHost(runningLocation: ExtensionRunningLocation, isInitialStart: boolean): IExtensionHost | null { switch (runningLocation.kind) { case ExtensionHostKind.LocalProcess: { - return this._instantiationService.createInstance(LocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); + return this._instantiationService.createInstance(SandboxLocalProcessExtensionHost, runningLocation, this._createLocalExtensionHostDataProvider(isInitialStart, runningLocation)); } case ExtensionHostKind.LocalWebWorker: { if (this._enableLocalWebWorker) { @@ -614,6 +612,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const sendTelemetry = (userReaction: 'install' | 'enable' | 'cancel') => { /* __GDPR__ "remoteExtensionRecommendations:popup" : { + "owner": "sandy081", "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } } @@ -676,8 +675,6 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { return remoteAuthority.substring(0, plusIndex); } -registerSingleton(IExtensionService, ExtensionService); - class RestartExtensionHostAction extends Action2 { constructor() { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts similarity index 56% rename from src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts rename to src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts index 0f7f60bbc7d..cee3a17b7f1 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-sandbox/localProcessExtensionHost.ts @@ -3,21 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Server, Socket, createServer } from 'net'; -import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; - import * as nls from 'vs/nls'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry, logRemoteEntryIfError } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; -import { PersistentProtocol } from 'vs/base/parts/ipc/common/ipc.net'; +import { BufferedEmitter } from 'vs/base/parts/ipc/common/ipc.net'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -27,7 +24,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { isUntitledWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { MessageType, createMessageOfType, isMessageOfType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { MessageType, isMessageOfType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; @@ -44,6 +41,8 @@ import { SerializedError } from 'vs/base/common/errors'; import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; +import { generateUuid } from 'vs/base/common/uuid'; +import { acquirePort } from 'vs/base/parts/ipc/electron-sandbox/ipc.mp'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -60,7 +59,7 @@ const enum NativeLogMarkers { End = 'END_NATIVE_LOG', } -class ExtensionHostProcess { +export class ExtensionHostProcess { private readonly _id: string; @@ -91,7 +90,7 @@ class ExtensionHostProcess { this._id = id; } - public start(opts: IExtensionHostProcessOptions): Promise<{ pid: number }> { + public start(opts: IExtensionHostProcessOptions): Promise { return this._extensionHostStarter.start(this._id, opts); } @@ -104,7 +103,7 @@ class ExtensionHostProcess { } } -export class LocalProcessExtensionHost implements IExtensionHost { +export class SandboxLocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; public readonly lazyStart = false; @@ -115,7 +114,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private readonly _onDidSetInspectPort = new Emitter(); - private readonly _toDispose = new DisposableStore(); + protected readonly _toDispose = new DisposableStore(); private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevDebug: boolean; @@ -127,11 +126,9 @@ export class LocalProcessExtensionHost implements IExtensionHost { private _terminating: boolean; // Resources, in order they get acquired/created when .start() is called: - private _namedPipeServer: Server | null; private _inspectPort: number | null; private _extensionHostProcess: ExtensionHostProcess | null; - private _extensionHostConnection: Socket | null; - private _messageProtocol: Promise | null; + private _messageProtocol: Promise | null; private readonly _extensionHostLogFile: URI; @@ -144,13 +141,13 @@ export class LocalProcessExtensionHost implements IExtensionHost { @ILifecycleService private readonly _lifecycleService: ILifecycleService, @INativeWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ILogService private readonly _logService: ILogService, + @ILogService protected readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, @IHostService private readonly _hostService: IHostService, @IProductService private readonly _productService: IProductService, @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService, - @IExtensionHostStarter private readonly _extensionHostStarter: IExtensionHostStarter, + @IExtensionHostStarter protected readonly _extensionHostStarter: IExtensionHostStarter, ) { const devOpts = parseExtensionDevOptions(this._environmentService); this._isExtensionDevHost = devOpts.isExtensionDevHost; @@ -161,17 +158,15 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._lastExtensionHostError = null; this._terminating = false; - this._namedPipeServer = null; this._inspectPort = null; this._extensionHostProcess = null; - this._extensionHostConnection = null; this._messageProtocol = null; this._extensionHostLogFile = joinPath(this._environmentService.extHostLogsPath, `${ExtensionHostLogFileName}.log`); this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._lifecycleService.onDidShutdown(() => this.terminate())); + this._toDispose.add(this._lifecycleService.onDidShutdown(() => this._terminate())); this._toDispose.add(this._extensionHostDebugService.onClose(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._nativeHostService.closeWindow(); @@ -185,7 +180,16 @@ export class LocalProcessExtensionHost implements IExtensionHost { } public dispose(): void { - this.terminate(); + this._terminate(); + } + + private _terminate(): void { + if (this._terminating) { + return; + } + this._terminating = true; + + this._toDispose.dispose(); } public start(): Promise | null { @@ -195,173 +199,166 @@ export class LocalProcessExtensionHost implements IExtensionHost { } if (!this._messageProtocol) { - this._messageProtocol = Promise.all([ - this._extensionHostStarter.createExtensionHost(), - this._tryListenOnPipe(), - this._tryFindDebugPort(), - this._shellEnvironmentService.getShellEnv(), - ]).then(([extensionHostCreationResult, pipeName, portNumber, processEnv]) => { - - this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); - - const env = objects.mixin(processEnv, { - VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', - VSCODE_PIPE_LOGGING: 'true', - VSCODE_VERBOSE_LOGGING: true, - VSCODE_LOG_NATIVE: this._isExtensionDevHost, - VSCODE_IPC_HOOK_EXTHOST: pipeName, - VSCODE_HANDLES_UNCAUGHT_ERRORS: true, - VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose) - }); - - if (this._environmentService.debugExtensionHost.env) { - objects.mixin(env, this._environmentService.debugExtensionHost.env); - } - - removeDangerousEnvVariables(env); - - if (this._isExtensionDevHost) { - // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might - // be that dependencies, that otherwise would be cached, get modified. - delete env['VSCODE_CODE_CACHE_PATH']; - } - - const opts = { - env, - // We only detach the extension host on windows. Linux and Mac orphan by default - // and detach under Linux and Mac create another process group. - // We detach because we have noticed that when the renderer exits, its child processes - // (i.e. extension host) are taken down in a brutal fashion by the OS - detached: !!platform.isWindows, - execArgv: undefined as string[] | undefined, - silent: true - }; - - if (portNumber !== 0) { - opts.execArgv = [ - '--nolazy', - (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber - ]; - } else { - opts.execArgv = ['--inspect-port=0']; - } - - if (this._environmentService.extensionTestsLocationURI) { - opts.execArgv.unshift('--expose-gc'); - } - - if (this._environmentService.args['prof-v8-extensions']) { - opts.execArgv.unshift('--prof'); - } - - if (this._environmentService.args['max-memory']) { - opts.execArgv.unshift(`--max-old-space-size=${this._environmentService.args['max-memory']}`); - } - - // Catch all output coming from the extension host process - type Output = { data: string; format: string[] }; - const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); - const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); - const onOutput = Event.any( - Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), - Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) - ); - - // Debounce all output, so we can render it in the Chrome console as a group - const onDebouncedOutput = Event.debounce(onOutput, (r, o) => { - return r - ? { data: r.data + o.data, format: [...r.format, ...o.format] } - : { data: o.data, format: o.format }; - }, 100); - - // Print out extension host output - onDebouncedOutput(output => { - const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); - if (inspectorUrlMatch) { - if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { - console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); - } - if (!this._inspectPort) { - this._inspectPort = Number(inspectorUrlMatch[2]); - this._onDidSetInspectPort.fire(); - } - } else { - if (!this._isExtensionDevTestFromCli) { - console.group('Extension Host'); - console.log(output.data, ...output.format); - console.groupEnd(); - } - } - }); - - // Support logging from extension host - this._extensionHostProcess.onMessage(msg => { - if (msg && (msg).type === '__$console') { - this._logExtensionHostMessage(msg); - } - }); - - // Lifecycle - - this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error)); - this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); - - // Notify debugger that we are ready to attach to the process if we run a development extension - if (portNumber) { - if (this._isExtensionDevHost && portNumber && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { - this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber); - } - this._inspectPort = portNumber; - this._onDidSetInspectPort.fire(); - } - - // Help in case we fail to start it - let startupTimeoutHandle: any; - if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) { - startupTimeoutHandle = setTimeout(() => { - this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`); - - const msg = this._isExtensionDevDebugBrk - ? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") - : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); - - this._notificationService.prompt(Severity.Warning, msg, - [{ - label: nls.localize('reloadWindow', "Reload Window"), - run: () => this._hostService.reload() - }], - { sticky: true } - ); - }, 10000); - } - - // Initialize extension host process with hand shakes - return this._tryExtHostHandshake(opts).then((protocol) => { - clearTimeout(startupTimeoutHandle); - return protocol; - }); - }); + this._messageProtocol = this._start(); } return this._messageProtocol; } - /** - * Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name. - */ - private _tryListenOnPipe(): Promise { - return new Promise((resolve, reject) => { - const pipeName = createRandomIPCHandle(); + protected async _start(): Promise { + const communication = this._toDispose.add(new ExtHostMessagePortCommunication(this._logService)); + return this._startWithCommunication(communication); + } - this._namedPipeServer = createServer(); - this._namedPipeServer.on('error', reject); - this._namedPipeServer.listen(pipeName, () => { - if (this._namedPipeServer) { - this._namedPipeServer.removeListener('error', reject); - } - resolve(pipeName); - }); + protected async _startWithCommunication(communication: IExtHostCommunication): Promise { + + const [extensionHostCreationResult, communicationPreparedData, portNumber, processEnv] = await Promise.all([ + this._extensionHostStarter.createExtensionHost(communication.useUtilityProcess), + communication.prepare(), + this._tryFindDebugPort(), + this._shellEnvironmentService.getShellEnv(), + ]); + + this._extensionHostProcess = new ExtensionHostProcess(extensionHostCreationResult.id, this._extensionHostStarter); + + const env = objects.mixin(processEnv, { + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/api/node/extensionHostProcess', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: true, + VSCODE_LOG_NATIVE: this._isExtensionDevHost, + VSCODE_HANDLES_UNCAUGHT_ERRORS: true, + VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose) }); + + if (this._environmentService.debugExtensionHost.env) { + objects.mixin(env, this._environmentService.debugExtensionHost.env); + } + + removeDangerousEnvVariables(env); + + if (this._isExtensionDevHost) { + // Unset `VSCODE_CODE_CACHE_PATH` when developing extensions because it might + // be that dependencies, that otherwise would be cached, get modified. + delete env['VSCODE_CODE_CACHE_PATH']; + } + + const opts: IExtensionHostProcessOptions = { + responseWindowId: this._environmentService.window.id, + responseChannel: 'vscode:startExtensionHostMessagePortResult', + responseNonce: generateUuid(), + env, + // We only detach the extension host on windows. Linux and Mac orphan by default + // and detach under Linux and Mac create another process group. + // We detach because we have noticed that when the renderer exits, its child processes + // (i.e. extension host) are taken down in a brutal fashion by the OS + detached: !!platform.isWindows, + execArgv: undefined as string[] | undefined, + silent: true + }; + + if (portNumber !== 0) { + opts.execArgv = [ + '--nolazy', + (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber + ]; + } else { + opts.execArgv = ['--inspect-port=0']; + } + + if (this._environmentService.extensionTestsLocationURI) { + opts.execArgv.unshift('--expose-gc'); + } + + if (this._environmentService.args['prof-v8-extensions']) { + opts.execArgv.unshift('--prof'); + } + + if (this._environmentService.args['max-memory']) { + opts.execArgv.unshift(`--max-old-space-size=${this._environmentService.args['max-memory']}`); + } + + // Catch all output coming from the extension host process + type Output = { data: string; format: string[] }; + const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.onStdout); + const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.onStderr); + const onOutput = Event.any( + Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), + Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) + ); + + // Debounce all output, so we can render it in the Chrome console as a group + const onDebouncedOutput = Event.debounce(onOutput, (r, o) => { + return r + ? { data: r.data + o.data, format: [...r.format, ...o.format] } + : { data: o.data, format: o.format }; + }, 100); + + // Print out extension host output + onDebouncedOutput(output => { + const inspectorUrlMatch = output.data && output.data.match(/ws:\/\/([^\s]+:(\d+)\/[^\s]+)/); + if (inspectorUrlMatch) { + if (!this._environmentService.isBuilt && !this._isExtensionDevTestFromCli) { + console.log(`%c[Extension Host] %cdebugger inspector at chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=${inspectorUrlMatch[1]}`, 'color: blue', 'color:'); + } + if (!this._inspectPort) { + this._inspectPort = Number(inspectorUrlMatch[2]); + this._onDidSetInspectPort.fire(); + } + } else { + if (!this._isExtensionDevTestFromCli) { + console.group('Extension Host'); + console.log(output.data, ...output.format); + console.groupEnd(); + } + } + }); + + // Support logging from extension host + this._extensionHostProcess.onMessage(msg => { + if (msg && (msg).type === '__$console') { + this._logExtensionHostMessage(msg); + } + }); + + // Lifecycle + + this._extensionHostProcess.onError((e) => this._onExtHostProcessError(e.error)); + this._extensionHostProcess.onExit(({ code, signal }) => this._onExtHostProcessExit(code, signal)); + + // Notify debugger that we are ready to attach to the process if we run a development extension + if (portNumber) { + if (this._isExtensionDevHost && portNumber && this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { + this._extensionHostDebugService.attachSession(this._environmentService.debugExtensionHost.debugId, portNumber); + } + this._inspectPort = portNumber; + this._onDidSetInspectPort.fire(); + } + + // Help in case we fail to start it + let startupTimeoutHandle: any; + if (!this._environmentService.isBuilt && !this._environmentService.remoteAuthority || this._isExtensionDevHost) { + startupTimeoutHandle = setTimeout(() => { + this._logService.error(`[LocalProcessExtensionHost]: Extension host did not start in 10 seconds (debugBrk: ${this._isExtensionDevDebugBrk})`); + + const msg = this._isExtensionDevDebugBrk + ? nls.localize('extensionHost.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") + : nls.localize('extensionHost.startupFail', "Extension host did not start in 10 seconds, that might be a problem."); + + this._notificationService.prompt(Severity.Warning, msg, + [{ + label: nls.localize('reloadWindow', "Reload Window"), + run: () => this._hostService.reload() + }], + { sticky: true } + ); + }, 10000); + } + + // Initialize extension host process with hand shakes + const protocol = await communication.establishProtocol(communicationPreparedData, this._extensionHostProcess, opts); + await this._performHandshake(protocol); + clearTimeout(startupTimeoutHandle); + return protocol; } /** @@ -394,102 +391,58 @@ export class LocalProcessExtensionHost implements IExtensionHost { return port || 0; } - private _tryExtHostHandshake(opts: IExtensionHostProcessOptions): Promise { + private _performHandshake(protocol: IMessagePassingProtocol): Promise { + // 1) wait for the incoming `ready` event and send the initialization data. + // 2) wait for the incoming `initialized` event. + return new Promise((resolve, reject) => { - return new Promise((resolve, reject) => { + let timeoutHandle: any; + const installTimeoutCheck = () => { + timeoutHandle = setTimeout(() => { + reject('The local extenion host took longer than 60s to send its ready message.'); + }, 60 * 1000); + }; + const uninstallTimeoutCheck = () => { + clearTimeout(timeoutHandle); + }; - // Wait for the extension host to connect to our named pipe - // and wrap the socket in the message passing protocol - let handle = setTimeout(() => { - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; + // Wait 60s for the ready message + installTimeoutCheck(); + + const disposable = protocol.onMessage(msg => { + + if (isMessageOfType(msg, MessageType.Ready)) { + + // 1) Extension Host is ready to receive messages, initialize it + uninstallTimeoutCheck(); + + this._createExtHostInitData().then(data => { + + // Wait 60s for the initialized message + installTimeoutCheck(); + + protocol.send(VSBuffer.fromString(JSON.stringify(data))); + }); + return; } - reject('The local extension host took longer than 60s to connect.'); - }, 60 * 1000); - this._namedPipeServer!.on('connection', socket => { + if (isMessageOfType(msg, MessageType.Initialized)) { - clearTimeout(handle); - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; + // 2) Extension Host is initialized + uninstallTimeoutCheck(); + + // stop listening for messages here + disposable.dispose(); + + // Register log channel for exthost log + Registry.as(Extensions.OutputChannels).registerChannel({ id: 'extHostLog', label: nls.localize('extension host Log', "Extension Host"), file: this._extensionHostLogFile, log: true }); + + // release this promise + resolve(); + return; } - this._extensionHostConnection = socket; - - // using a buffered message protocol here because between now - // and the first time a `then` executes some messages might be lost - // unless we immediately register a listener for `onMessage`. - resolve(new PersistentProtocol(new NodeSocket(this._extensionHostConnection, 'renderer-exthost'))); - }); - - // Now that the named pipe listener is installed, start the ext host process - const sw = StopWatch.create(false); - this._extensionHostProcess!.start(opts).then(() => { - const duration = sw.elapsed(); - if (platform.isCI) { - this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); - } - }, (err) => { - // Starting the ext host process resulted in an error - reject(err); - }); - - }).then((protocol) => { - - // 1) wait for the incoming `ready` event and send the initialization data. - // 2) wait for the incoming `initialized` event. - return new Promise((resolve, reject) => { - - let timeoutHandle: NodeJS.Timer; - const installTimeoutCheck = () => { - timeoutHandle = setTimeout(() => { - reject('The local extenion host took longer than 60s to send its ready message.'); - }, 60 * 1000); - }; - const uninstallTimeoutCheck = () => { - clearTimeout(timeoutHandle); - }; - - // Wait 60s for the ready message - installTimeoutCheck(); - - const disposable = protocol.onMessage(msg => { - - if (isMessageOfType(msg, MessageType.Ready)) { - - // 1) Extension Host is ready to receive messages, initialize it - uninstallTimeoutCheck(); - - this._createExtHostInitData().then(data => { - - // Wait 60s for the initialized message - installTimeoutCheck(); - - protocol.send(VSBuffer.fromString(JSON.stringify(data))); - }); - return; - } - - if (isMessageOfType(msg, MessageType.Initialized)) { - - // 2) Extension Host is initialized - uninstallTimeoutCheck(); - - // stop listening for messages here - disposable.dispose(); - - // Register log channel for exthost log - Registry.as(Extensions.OutputChannels).registerChannel({ id: 'extHostLog', label: nls.localize('extension host Log', "Extension Host"), file: this._extensionHostLogFile, log: true }); - - // release this promise - resolve(protocol); - return; - } - - console.error(`received unexpected message during handshake phase from the extension host: `, msg); - }); + console.error(`received unexpected message during handshake phase from the extension host: `, msg); }); }); @@ -632,58 +585,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { return withNullAsUndefined(this._inspectPort); } - private terminate(): void { - if (this._terminating) { - return; - } - this._terminating = true; - - this._toDispose.dispose(); - - if (!this._messageProtocol) { - // .start() was not called - return; - } - - this._messageProtocol.then((protocol) => { - - // Send the extension host a request to terminate itself - // (graceful termination) - protocol.send(createMessageOfType(MessageType.Terminate)); - - protocol.getSocket().dispose(); - - protocol.dispose(); - - // Give the extension host 10s, after which we will - // try to kill the process and release any resources - setTimeout(() => this._cleanResources(), 10 * 1000); - - }, (err) => { - - // Establishing a protocol with the extension host failed, so - // try to kill the process and release any resources. - this._cleanResources(); - }); - } - - private _cleanResources(): void { - if (this._namedPipeServer) { - this._namedPipeServer.close(); - this._namedPipeServer = null; - } - if (this._extensionHostConnection) { - this._extensionHostConnection.end(); - this._extensionHostConnection = null; - } - if (this._extensionHostProcess) { - this._extensionHostProcess.kill(); - this._extensionHostProcess = null; - } - } - private _onWillShutdown(event: WillShutdownEvent): void { - // If the extension development host was started without debugger attached we need // to communicate this back to the main side to terminate the debug session if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { @@ -692,3 +594,63 @@ export class LocalProcessExtensionHost implements IExtensionHost { } } } + +export interface IExtHostCommunication { + readonly useUtilityProcess: boolean; + prepare(): Promise; + establishProtocol(prepared: T, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise; +} + +export class ExtHostMessagePortCommunication extends Disposable implements IExtHostCommunication { + + readonly useUtilityProcess = true; + + constructor( + @ILogService private readonly _logService: ILogService + ) { + super(); + } + + async prepare(): Promise { + } + + establishProtocol(prepared: void, extensionHostProcess: ExtensionHostProcess, opts: IExtensionHostProcessOptions): Promise { + + opts.env['VSCODE_WILL_SEND_MESSAGE_PORT'] = 'true'; + + // Get ready to acquire the message port from the shared process worker + const portPromise = acquirePort(undefined /* we trigger the request via service call! */, opts.responseChannel, opts.responseNonce); + + return new Promise((resolve, reject) => { + + const handle = setTimeout(() => { + reject('The local extension host took longer than 60s to connect.'); + }, 60 * 1000); + + portPromise.then((port) => { + clearTimeout(handle); + + const onMessage = new BufferedEmitter(); + port.onmessage = ((e) => onMessage.fire(VSBuffer.wrap(e.data))); + port.start(); + + resolve({ + onMessage: onMessage.event, + send: message => port.postMessage(message.buffer), + }); + }); + + // Now that the message port listener is installed, start the ext host process + const sw = StopWatch.create(false); + extensionHostProcess.start(opts).then(() => { + const duration = sw.elapsed(); + if (platform.isCI) { + this._logService.info(`IExtensionHostStarter.start() took ${duration} ms.`); + } + }, (err) => { + // Starting the ext host process resulted in an error + reject(err); + }); + }); + } +} diff --git a/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.ts b/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.ts new file mode 100644 index 00000000000..ea51d7abc8f --- /dev/null +++ b/src/vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService.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 { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ElectronExtensionService } from 'vs/workbench/services/extensions/electron-sandbox/electronExtensionService'; + +export class SandboxExtensionService extends ElectronExtensionService { +} + +registerSingleton(IExtensionService, SandboxExtensionService); diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index 837a4bdfd52..24f4173e3f8 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -230,7 +230,19 @@ export class HoverWidget extends Widget { this._hover.containerDomNode.classList.remove('right-aligned'); this._hover.contentsDomNode.style.maxHeight = ''; - const targetBounds = this._target.targetElements.map(e => e.getBoundingClientRect()); + const getZoomAccountedBoundingClientRect = (e: HTMLElement) => { + const zoom = dom.getDomNodeZoomLevel(e); + + const boundingRect = e.getBoundingClientRect(); + return { + top: boundingRect.top * zoom, + bottom: boundingRect.bottom * zoom, + right: boundingRect.right * zoom, + left: boundingRect.left * zoom, + }; + }; + + const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e)); const top = Math.min(...targetBounds.map(e => e.top)); const right = Math.max(...targetBounds.map(e => e.right)); const bottom = Math.max(...targetBounds.map(e => e.bottom)); diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index d3428409896..e70dd761951 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -203,6 +203,7 @@ export class LanguageDetectionWorkerHost { async sendTelemetryEvent(languages: string[], confidences: number[], timeSpent: number): Promise { type LanguageDetectionStats = { languages: string; confidences: string; timeSpent: number }; type LanguageDetectionStatsClassification = { + owner: 'TylerLeonhardt'; languages: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; confidences: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; @@ -337,6 +338,7 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { } type LanguageDetectionPerfClassification = { + owner: 'TylerLeonhardt'; timeSpent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; detection: { classification: 'SystemMetaData'; purpose: 'FeatureInsight' }; }; diff --git a/src/vs/workbench/services/localization/common/locale.ts b/src/vs/workbench/services/localization/common/locale.ts new file mode 100644 index 00000000000..1b67af98c04 --- /dev/null +++ b/src/vs/workbench/services/localization/common/locale.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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const ILocaleService = createDecorator('localizationService'); + +export interface ILocaleService { + readonly _serviceBrand: undefined; + setLocale(languagePackItem: string | undefined): Promise; +} diff --git a/src/vs/workbench/services/localizations/electron-sandbox/localizationsService.ts b/src/vs/workbench/services/localization/electron-sandbox/languagePackService.ts similarity index 68% rename from src/vs/workbench/services/localizations/electron-sandbox/localizationsService.ts rename to src/vs/workbench/services/localization/electron-sandbox/languagePackService.ts index 5b716004793..b71303b3a90 100644 --- a/src/vs/workbench/services/localizations/electron-sandbox/localizationsService.ts +++ b/src/vs/workbench/services/localization/electron-sandbox/languagePackService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { ILanguagePackService } from 'vs/platform/languagePacks/common/languagePacks'; import { registerSharedProcessRemoteService } from 'vs/platform/ipc/electron-sandbox/services'; -registerSharedProcessRemoteService(ILocalizationsService, 'localizations', { supportsDelayedInstantiation: true }); +registerSharedProcessRemoteService(ILanguagePackService, 'languagePacks', { supportsDelayedInstantiation: true }); diff --git a/src/vs/workbench/services/localization/electron-sandbox/localeService.ts b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts new file mode 100644 index 00000000000..d435b3725d5 --- /dev/null +++ b/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { language } from 'vs/base/common/platform'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; +import { ILocaleService } from 'vs/workbench/services/localization/common/locale'; + +export class NativeLocaleService implements ILocaleService { + _serviceBrand: undefined; + + constructor( + @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @INotificationService private readonly notificationService: INotificationService, + ) { } + + async setLocale(locale: string | undefined): Promise { + try { + if (locale === language || (!locale && language === 'en')) { + return false; + } + await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); + return true; + } catch (err) { + this.notificationService.error(err); + return false; + } + } +} + +registerSingleton(ILocaleService, NativeLocaleService, true); diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index 555a3a7ee20..27785f847cc 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -304,6 +304,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic async openGlobalKeybindingSettings(textual: boolean, options?: IKeybindingsEditorOptions): Promise { type OpenKeybindingsClassification = { + owner: 'sandy081'; textual: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; }; this.telemetryService.publicLog2<{ textual: boolean }, OpenKeybindingsClassification>('openKeybindings', { textual }); diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 5aaf1198fe4..06b43562999 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -87,7 +87,7 @@ export interface ISetting { enumItemLabels?: string[]; allKeysAreBoolean?: boolean; editPresentation?: EditPresentationTypes; - defaultValueSource?: string | IExtensionInfo; + nonLanguageSpecificDefaultValueSource?: string | IExtensionInfo; isLanguageTagSetting?: boolean; categoryOrder?: number; categoryLabel?: string; diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 000a74b77ca..9fb02061b6e 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -707,17 +707,19 @@ export class DefaultSettings extends Disposable { }); } - const registeredConfigurationProp = prop as IRegisteredConfigurationPropertySchema; - let defaultValueSource: string | IExtensionInfo | undefined; - if (registeredConfigurationProp && registeredConfigurationProp.defaultValueSource) { - defaultValueSource = registeredConfigurationProp.defaultValueSource; - } - let isLanguageTagSetting = false; if (OVERRIDE_PROPERTY_REGEX.test(key)) { isLanguageTagSetting = true; } + let defaultValueSource: string | IExtensionInfo | undefined; + if (!isLanguageTagSetting) { + const registeredConfigurationProp = prop as IRegisteredConfigurationPropertySchema; + if (registeredConfigurationProp && registeredConfigurationProp.defaultValueSource) { + defaultValueSource = registeredConfigurationProp.defaultValueSource; + } + } + result.push({ key, value, @@ -749,7 +751,7 @@ export class DefaultSettings extends Disposable { allKeysAreBoolean, editPresentation: prop.editPresentation, order: prop.order, - defaultValueSource, + nonLanguageSpecificDefaultValueSource: defaultValueSource, isLanguageTagSetting, categoryLabel, categoryOrder diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 3268cc37382..0eba0de6b00 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IChannel, IServerChannel, getDelayedChannel, IPCLogger } from 'vs/base/parts/ipc/common/ipc'; import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; +import { connectRemoteAgentManagement, IConnectionOptions, ISocketFactory, ManagementPersistentConnection, PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection'; import { IExtensionHostExitInfo, IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { RemoteAgentConnectionContext, IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; @@ -125,6 +125,17 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I ); } + getRoundTripTime(): Promise { + return this._withTelemetryChannel( + async channel => { + const start = Date.now(); + await RemoteExtensionEnvironmentChannelClient.ping(channel); + return Date.now() - start; + }, + undefined + ); + } + private _withChannel(callback: (channel: IChannel, connection: IRemoteAgentConnection) => Promise, fallback: R): Promise { const connection = this.getConnection(); if (!connection) { @@ -153,6 +164,8 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon readonly remoteAuthority: string; private _connection: Promise> | null; + private _initialConnectionMs: number | undefined; + constructor( remoteAuthority: string, private readonly _commit: string | undefined, @@ -181,6 +194,16 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon this._getOrCreateConnection().then(client => client.registerChannel(channelName, channel)); } + async getInitialConnectionTimeMs() { + try { + await this._getOrCreateConnection(); + } catch { + // ignored -- time is measured even if connection fails + } + + return this._initialConnectionMs!; + } + private _getOrCreateConnection(): Promise> { if (!this._connection) { this._connection = this._createConnection(); @@ -209,7 +232,14 @@ export class RemoteAgentConnection extends Disposable implements IRemoteAgentCon logService: this._logService, ipcLogger: false ? new IPCLogger(`Local \u2192 Remote`, `Remote \u2192 Local`) : null }; - const connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); + let connection: ManagementPersistentConnection; + let start = Date.now(); + try { + connection = this._register(await connectRemoteAgentManagement(options, this.remoteAuthority, `renderer`)); + } finally { + this._initialConnectionMs = Date.now() - start; + } + connection.protocol.onDidDispose(() => { connection.dispose(); }); diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 5d33f408514..35bfae00a4d 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -138,4 +138,8 @@ export class RemoteExtensionEnvironmentChannelClient { static flushTelemetry(channel: IChannel): Promise { return channel.call('flushTelemetry'); } + + static async ping(channel: IChannel): Promise { + await channel.call('ping'); + } } diff --git a/src/vs/workbench/services/remote/common/remoteAgentService.ts b/src/vs/workbench/services/remote/common/remoteAgentService.ts index b7508ffc4a7..6a899e927be 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentService.ts @@ -36,6 +36,12 @@ export interface IRemoteAgentService { */ getExtensionHostExitInfo(reconnectionToken: string): Promise; + /** + * Gets the round trip time from the remote extension host. Note that this + * may be delayed if the extension host is busy. + */ + getRoundTripTime(): Promise; + whenExtensionsReady(): Promise; /** * Scan remote extensions. @@ -65,4 +71,5 @@ export interface IRemoteAgentConnection { getChannel(channelName: string): T; withChannel(channelName: string, callback: (channel: T) => Promise): Promise; registerChannel>(channelName: string, channel: T): void; + getInitialConnectionTimeMs(): Promise; } diff --git a/src/vs/workbench/services/search/common/ignoreFile.ts b/src/vs/workbench/services/search/common/ignoreFile.ts index a1a0b181a01..35c4dea27ff 100644 --- a/src/vs/workbench/services/search/common/ignoreFile.ts +++ b/src/vs/workbench/services/search/common/ignoreFile.ts @@ -11,6 +11,12 @@ export class IgnoreFile { private isPathIgnored: (path: string, isDir: boolean, parent?: IgnoreFile) => boolean; constructor(contents: string, location: string, parent?: IgnoreFile) { + if (location[location.length - 1] === '\\') { + throw Error('Unexpected path format, do not use trailing backslashes'); + } + if (location[location.length - 1] !== '/') { + location += '/'; + } this.isPathIgnored = this.parseIgnoreFile(contents, location, parent); } diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 282bbfbe40e..cf0aad3563a 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -288,6 +288,7 @@ export class SearchService extends Disposable implements ISearchService { const cacheStats: ICachedSearchStats = fileSearchStats.detailStats as ICachedSearchStats; type CachedSearchCompleteClassifcation = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; resultCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; @@ -330,6 +331,7 @@ export class SearchService extends Disposable implements ISearchService { const searchEngineStats: ISearchEngineStats = fileSearchStats.detailStats as ISearchEngineStats; type SearchCompleteClassification = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; resultCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; @@ -387,6 +389,7 @@ export class SearchService extends Disposable implements ISearchService { } type TextSearchCompleteClassification = { + owner: 'roblourens'; reason?: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; workspaceFolderCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; endToEndTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 0df6157e1bd..296cf73f0bc 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -12,7 +12,8 @@ import * as resources from 'vs/base/common/resources'; import * as types from 'vs/base/common/types'; import { equals as equalArray } from 'vs/base/common/arrays'; import { URI } from 'vs/base/common/uri'; -import { IState, ITokenizationSupport, LanguageId, TokenizationRegistry, StandardTokenType, ITokenizationSupportFactory, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { IState, ITokenizationSupport, TokenizationRegistry, ITokenizationSupportFactory, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { LanguageId, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; import { nullTokenizeEncoded } from 'vs/editor/common/languages/nullTokenize'; import { generateTokensCSSForColorMap } from 'vs/editor/common/languages/supports/tokenization'; import { ILanguageService } from 'vs/editor/common/languages/language'; diff --git a/src/vs/workbench/services/textMate/browser/textMateWorker.ts b/src/vs/workbench/services/textMate/browser/textMateWorker.ts index 6b848d33c24..c4d6299742d 100644 --- a/src/vs/workbench/services/textMate/browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/browser/textMateWorker.ts @@ -5,7 +5,7 @@ import { IWorkerContext } from 'vs/editor/common/services/editorSimpleWorker'; import { UriComponents, URI } from 'vs/base/common/uri'; -import { LanguageId } from 'vs/editor/common/languages'; +import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import { IValidEmbeddedLanguagesMap, IValidTokenTypeMap, IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; import { TMGrammarFactory, ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IModelChangedEvent, MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; diff --git a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts index 83b041e7823..5d88a28f4f4 100644 --- a/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts +++ b/src/vs/workbench/services/textMate/common/TMScopeRegistry.ts @@ -6,7 +6,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Disposable } from 'vs/base/common/lifecycle'; -import { StandardTokenType, LanguageId } from 'vs/editor/common/languages'; +import { LanguageId, StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; export interface IValidGrammarDefinition { location: URI; diff --git a/src/vs/workbench/services/textMate/common/TMTokenization.ts b/src/vs/workbench/services/textMate/common/TMTokenization.ts index 64816e5d912..058115fb065 100644 --- a/src/vs/workbench/services/textMate/common/TMTokenization.ts +++ b/src/vs/workbench/services/textMate/common/TMTokenization.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { IState, ITokenizationSupport, LanguageId, TokenMetadata, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { IState, ITokenizationSupport, TokenizationResult, EncodedTokenizationResult } from 'vs/editor/common/languages'; +import { LanguageId, TokenMetadata } from 'vs/editor/common/encodedTokenAttributes'; import type { IGrammar, StackElement } from 'vscode-textmate'; import { Disposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 901f92d9f3e..e2b130592fb 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -579,6 +579,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { const key = themeType + themeData.extensionId; if (!this.themeExtensionsActivated.get(key)) { type ActivatePluginClassification = { + owner: 'aeschli'; id: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; name: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; isBuiltin: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index be619fd1188..08723845be3 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -514,6 +514,7 @@ export abstract class AbstractTimerService implements ITimerService { // report IStartupMetrics as telemetry /* __GDPR__ "startupTimeVaried" : { + "owner": "jrieken", "${include}": [ "${IStartupMetrics}" ] diff --git a/src/vs/workbench/services/title/common/titleService.ts b/src/vs/workbench/services/title/common/titleService.ts index a89bb2f096a..92aca9f92cd 100644 --- a/src/vs/workbench/services/title/common/titleService.ts +++ b/src/vs/workbench/services/title/common/titleService.ts @@ -26,7 +26,7 @@ export interface ITitleService { /** * Title menu is visible */ - readonly titleMenuVisible: boolean; + readonly isCommandCenterVisible: boolean; /** * An event when the title menu is enabled/disabled diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 482be496d9b..d90996fd7ab 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -37,11 +37,13 @@ import { ICredentialsService } from 'vs/platform/credentials/common/credentials' import { CancellationError } from 'vs/base/common/errors'; type UserAccountClassification = { + owner: 'sandy081'; id: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; providerId: { classification: 'EndUserPseudonymizedInformation'; purpose: 'BusinessInsight' }; }; type FirstTimeSyncClassification = { + owner: 'sandy081'; action: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true }; }; @@ -617,7 +619,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async onDidSuccessiveAuthFailures(): Promise { - this.telemetryService.publicLog2('sync/successiveAuthFailures'); + this.telemetryService.publicLog2<{}, { owner: 'sandy081' }>('sync/successiveAuthFailures'); this.currentSessionId = undefined; await this.update(); diff --git a/src/vs/workbench/services/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts index 988300aadee..052cc97c7e0 100644 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ b/src/vs/workbench/services/views/browser/treeViewsService.ts @@ -5,10 +5,10 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { VSDataTransfer } from 'vs/base/common/dataTransfer'; 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/workspaces/browser/abstractWorkspaceEditingService.ts b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts index ebe492e36ed..5e13ba82ef2 100644 --- a/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts @@ -76,23 +76,20 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi } private getNewWorkspaceName(): string { - switch (this.contextService.getWorkbenchState()) { - case WorkbenchState.FOLDER: { - const folder = firstOrDefault(this.contextService.getWorkspace().folders); - if (folder) { - return `${basename(folder.uri)}.${WORKSPACE_EXTENSION}`; - } - break; - } - case WorkbenchState.WORKSPACE: { - const configPathURI = this.getCurrentWorkspaceIdentifier()?.configPath; - if (configPathURI && isSavedWorkspace(configPathURI, this.environmentService)) { - return basename(configPathURI); - } - break; - } + + // First try with existing workspace name + const configPathURI = this.getCurrentWorkspaceIdentifier()?.configPath; + if (configPathURI && isSavedWorkspace(configPathURI, this.environmentService)) { + return basename(configPathURI); } + // Then fallback to first folder if any + const folder = firstOrDefault(this.contextService.getWorkspace().folders); + if (folder) { + return `${basename(folder.uri)}.${WORKSPACE_EXTENSION}`; + } + + // Finally pick a good default return `workspace.${WORKSPACE_EXTENSION}`; } diff --git a/src/vs/workbench/services/workspaces/common/workspaceEditing.ts b/src/vs/workbench/services/workspaces/common/workspaceEditing.ts index 6ae82b2fa00..8ca4f01f6d4 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceEditing.ts @@ -33,28 +33,28 @@ export interface IWorkspaceEditingService { updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise; /** - * enters the workspace with the provided path. + * Enters the workspace with the provided path. */ enterWorkspace(path: URI): Promise; /** - * creates a new workspace with the provided folders and opens it. if path is provided + * Creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. */ createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise; /** - * saves the current workspace to the provided path and opens it. requires a workspace to be opened. + * Saves the current workspace to the provided path and opens it. requires a workspace to be opened. */ saveAndEnterWorkspace(path: URI): Promise; /** - * copies current workspace settings to the target workspace. + * Copies current workspace settings to the target workspace. */ copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise; /** - * picks a new workspace path + * Picks a new workspace path */ pickNewWorkspacePath(): Promise; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 9a1025debd8..5bd962101c9 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1920,4 +1920,5 @@ export class TestRemoteAgentService implements IRemoteAgentService { async updateTelemetryLevel(telemetryLevel: TelemetryLevel): Promise { } async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } async flushTelemetry(): Promise { } + async getRoundTripTime(): Promise { return undefined; } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index bf87db19d53..a9e13a7d31d 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -210,6 +210,9 @@ import 'vs/workbench/contrib/debug/browser/debugViewlet'; // Markers import 'vs/workbench/contrib/markers/browser/markers.contribution'; +// Merge Editor +import 'vs/workbench/contrib/mergeEditor/browser/mergeEditor.contribution'; + // Comments import 'vs/workbench/contrib/comments/browser/comments.contribution'; @@ -343,7 +346,4 @@ import 'vs/workbench/contrib/list/browser/list.contribution'; // Audio Cues import 'vs/workbench/contrib/audioCues/browser/audioCues.contribution'; -// Drop into editor -import 'vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution'; - //#endregion diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 75b3b27affa..6b7b44d4155 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -37,7 +37,7 @@ import 'vs/workbench/workbench.sandbox.main'; // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -import 'vs/workbench/services/extensions/electron-browser/extensionService'; +import 'vs/workbench/services/extensions/electron-browser/nativeExtensionService'; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts index 79258547199..894e6785212 100644 --- a/src/vs/workbench/workbench.desktop.sandbox.main.ts +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -27,12 +27,6 @@ import 'vs/workbench/electron-sandbox/desktop.main'; //#region --- workbench services -import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; - -// TODO@bpasero sandbox: remove me when extension host is present -class SimpleExtensionService extends NullExtensionService { } - -registerSingleton(IExtensionService, SimpleExtensionService); +import 'vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService'; //#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index e0708193c34..41435143c48 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -58,7 +58,7 @@ import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionMana import 'vs/workbench/services/extensionManagement/electron-sandbox/extensionUrlTrustService'; import 'vs/workbench/services/credentials/electron-sandbox/credentialsService'; import 'vs/workbench/services/encryption/electron-sandbox/encryptionService'; -import 'vs/workbench/services/localizations/electron-sandbox/localizationsService'; +import 'vs/workbench/services/localization/electron-sandbox/languagePackService'; import 'vs/workbench/services/telemetry/electron-sandbox/telemetryService'; import 'vs/workbench/services/extensions/electron-sandbox/extensionHostStarter'; import 'vs/platform/extensionManagement/electron-sandbox/extensionsScannerService'; @@ -83,6 +83,7 @@ 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 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; +import 'vs/workbench/services/localization/electron-sandbox/localeService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; @@ -98,7 +99,7 @@ registerSingleton(IUserDataInitializationService, UserDataInitializationService) import 'vs/workbench/contrib/logs/electron-sandbox/logs.contribution'; // Localizations -import 'vs/workbench/contrib/localizations/browser/localizations.contribution'; +import 'vs/workbench/contrib/localization/electron-sandbox/localization.contribution'; // Explorer import 'vs/workbench/contrib/files/electron-sandbox/files.contribution'; diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index a34508112e7..5421d3dced1 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -766,8 +766,9 @@ declare module 'vscode' { /** * An optional flag that controls if an {@link TextEditor editor}-tab shows as preview. Preview tabs will - * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends - * on the `workbench.editor.enablePreview`-setting. + * be replaced and reused until set to stay - either explicitly or through editing. + * + * *Note* that the flag is ignored if a user has disabled preview editors in settings. */ preview?: boolean; @@ -777,6 +778,67 @@ declare module 'vscode' { selection?: Range; } + /** + * Represents an event describing the change in a {@link NotebookEditor.selections notebook editor's selections}. + */ + export interface NotebookEditorSelectionChangeEvent { + /** + * The {@link NotebookEditor notebook editor} for which the selections have changed. + */ + readonly notebookEditor: NotebookEditor; + + /** + * The new value for the {@link NotebookEditor.selections notebook editor's selections}. + */ + readonly selections: readonly NotebookRange[]; + } + + /** + * Represents an event describing the change in a {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. + */ + export interface NotebookEditorVisibleRangesChangeEvent { + /** + * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. + */ + readonly notebookEditor: NotebookEditor; + + /** + * The new value for the {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. + */ + readonly visibleRanges: readonly NotebookRange[]; + } + + /** + * Represents options to configure the behavior of showing a {@link NotebookDocument notebook document} in an {@link NotebookEditor notebook editor}. + */ + export interface NotebookDocumentShowOptions { + /** + * An optional view column in which the {@link NotebookEditor notebook editor} should be shown. + * The default is the {@link ViewColumn.Active active}, other values are adjusted to + * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is + * not adjusted. Use {@linkcode ViewColumn.Beside} to open the + * editor to the side of the currently active one. + */ + readonly viewColumn?: ViewColumn; + + /** + * An optional flag that when `true` will stop the {@link NotebookEditor notebook editor} from taking focus. + */ + readonly preserveFocus?: boolean; + + /** + * An optional flag that controls if an {@link NotebookEditor notebook editor}-tab shows as preview. Preview tabs will + * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends + * on the `workbench.editor.enablePreview`-setting. + */ + readonly preview?: boolean; + + /** + * An optional selection to apply for the document in the {@link NotebookEditor notebook editor}. + */ + readonly selections?: readonly NotebookRange[]; + } + /** * A reference to one of the workbench colors as defined in https://code.visualstudio.com/docs/getstarted/theme-color-reference. * Using a theme color is preferred over a custom color as it gives theme authors and users the possibility to change the color. @@ -4437,6 +4499,144 @@ declare module 'vscode' { resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult; } + + /** + * The inline completion item provider interface defines the contract between extensions and + * the inline completion feature. + * + * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. + */ + export interface InlineCompletionItemProvider { + + /** + * 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 explicitly asks for the next or previous inline completion. + * In that case, all available inline completions should be returned. + * `context.triggerKind` can be used to distinguish between these scenarios. + * + * @param document The document inline completions are requested for. + * @param position The position inline completions are requested for. + * @param context A context object with additional information. + * @param token A cancellation token. + * @return An array of completion items or a thenable that resolves to an array of completion items. + */ + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + } + + /** + * Represents a collection of {@link InlineCompletionItem inline completion items} to be presented + * in the editor. + */ + export class InlineCompletionList { + /** + * The inline completion items. + */ + items: InlineCompletionItem[]; + + /** + * Creates a new list of inline completion items. + */ + constructor(items: InlineCompletionItem[]); + } + + /** + * Provides information about the context in which an inline completion was requested. + */ + export interface InlineCompletionContext { + /** + * Describes how the inline completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + + /** + * 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. + */ + readonly selectedCompletionInfo: SelectedCompletionInfo | undefined; + } + + /** + * Describes the currently selected completion item. + */ + export interface SelectedCompletionInfo { + /** + * The range that will be replaced if this completion item is accepted. + */ + readonly range: Range; + + /** + * The text the range will be replaced with if this completion is accepted. + */ + readonly text: string; + } + + /** + * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * 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, + } + + /** + * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. + * + * @see {@link InlineCompletionItemProvider.provideInlineCompletionItems} + */ + export class InlineCompletionItem { + /** + * The text to replace the range with. Must be set. + * Is used both for the preview and the accept operation. + */ + insertText: string | SnippetString; + + /** + * A text that is used to decide if this inline completion should be shown. When `falsy` + * the {@link InlineCompletionItem.insertText} is used. + * + * An inline completion is shown if the text to replace is a prefix of the filter text. + */ + filterText?: string; + + /** + * The range to replace. + * Must begin and end on the same line. + * + * Prefer replacements over insertions to provide a better experience when the user deletes typed text. + */ + range?: Range; + + /** + * An optional {@link Command} that is executed *after* inserting this completion. + */ + command?: Command; + + /** + * Creates a new inline completion item. + * + * @param insertText The text to replace the range with. + * @param range The range to replace. If not set, the word at the requested position will be used. + * @param command An optional {@link Command} that is executed *after* inserting this completion. + */ + constructor(insertText: string | SnippetString, range?: Range, command?: Command); + } + /** * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. @@ -9121,6 +9321,43 @@ declare module 'vscode' { */ export const onDidChangeTextEditorViewColumn: Event; + /** + * The currently visible {@link NotebookEditor notebook editors} or an empty array. + */ + export const visibleNotebookEditors: readonly NotebookEditor[]; + + /** + * An {@link Event} which fires when the {@link window.visibleNotebookEditors visible notebook editors} + * has changed. + */ + export const onDidChangeVisibleNotebookEditors: Event; + + /** + * The currently active {@link NotebookEditor notebook editor} or `undefined`. The active editor is the one + * that currently has focus or, when none has focus, the one that has changed + * input most recently. + */ + export const activeNotebookEditor: NotebookEditor | undefined; + + /** + * An {@link Event} which fires when the {@link window.activeNotebookEditor active notebook editor} + * has changed. *Note* that the event also fires when the active editor changes + * to `undefined`. + */ + export const onDidChangeActiveNotebookEditor: Event; + + /** + * An {@link Event} which fires when the {@link NotebookEditor.selections notebook editor selections} + * have changed. + */ + export const onDidChangeNotebookEditorSelection: Event; + + /** + * An {@link Event} which fires when the {@link NotebookEditor.visibleRanges notebook editor visible ranges} + * have changed. + */ + export const onDidChangeNotebookEditorVisibleRanges: Event; + /** * The currently opened terminals or an empty array. */ @@ -9200,6 +9437,16 @@ declare module 'vscode' { */ export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; + /** + * Show the given {@link NotebookDocument} in a {@link NotebookEditor notebook editor}. + * + * @param document A text document to be shown. + * @param options {@link NotebookDocumentShowOptions Editor options} to configure the behavior of showing the {@link NotebookEditor notebook editor}. + * + * @return A promise that resolves to an {@link NotebookEditor notebook editor}. + */ + export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; + /** * Create a TextEditorDecorationType that can be used to add decorations to text editors. * @@ -12094,6 +12341,19 @@ declare module 'vscode' { */ export function registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; + /** + * Registers an inline completion provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline completion provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; + /** * Register a code action provider. * @@ -12479,7 +12739,32 @@ declare module 'vscode' { * @return A {@link Disposable} that unsets this configuration. */ export function setLanguageConfiguration(language: string, configuration: LanguageConfiguration): Disposable; + } + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + */ + export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 } /** @@ -12489,6 +12774,40 @@ declare module 'vscode' { */ export interface NotebookEditor { + /** + * The {@link NotebookDocument notebook document} associated with this notebook editor. + */ + readonly notebook: NotebookDocument; + + /** + * The primary selection in this notebook editor. + */ + selection: NotebookRange; + + /** + * All selections in this notebook editor. + * + * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; + */ + selections: readonly NotebookRange[]; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: readonly NotebookRange[]; + + /** + * The column in which this editor shows. + */ + readonly viewColumn?: ViewColumn; + + /** + * Scroll as indicated by `revealType` in order to reveal the given range. + * + * @param range A range. + * @param revealType The scrolling strategy for revealing `range`. + */ + revealRange(range: NotebookRange, revealType?: NotebookEditorRevealType): void; } /** diff --git a/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts b/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts new file mode 100644 index 00000000000..323ff90cecb --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder declaration for the `mergeEditor/toolbar` menu diff --git a/src/vscode-dts/vscode.proposed.documentPaste.d.ts b/src/vscode-dts/vscode.proposed.documentPaste.d.ts new file mode 100644 index 00000000000..1777c8d4145 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.documentPaste.d.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * 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/30066/ + + /** + * Provider invoked when the user copies and pastes code. + */ + interface DocumentPasteEditProvider { + + /** + * Optional method invoked after the user copies text in a file. + * + * During {@link prepareDocumentPaste}, an extension can compute metadata that is attached to + * a {@link DataTransfer} and is passed back to the provider in {@link provideDocumentPasteEdits}. + * + * @param document Document where the copy took place. + * @param range Range being copied in the `document`. + * @param dataTransfer The data transfer associated with the copy. You can store additional values on this for later use in {@link provideDocumentPasteEdits}. + * @param token A cancellation token. + */ + prepareDocumentPaste?(document: TextDocument, range: Range, dataTransfer: DataTransfer, token: CancellationToken): void | Thenable; + + /** + * Invoked before the user pastes into a document. + * + * In this method, extensions can return a workspace edit that replaces the standard pasting behavior. + * + * @param document Document being pasted into + * @param range Currently selected range in the document. + * @param dataTransfer The data transfer associated with the paste. + * @param token A cancellation token. + * + * @return Optional workspace edit that applies the paste. Return undefined to use standard pasting. + */ + provideDocumentPasteEdits(document: TextDocument, range: Range, dataTransfer: DataTransfer, token: CancellationToken): ProviderResult; + } + + namespace languages { + export function registerDocumentPasteEditProvider(selector: DocumentSelector, provider: DocumentPasteEditProvider): Disposable; + } +} diff --git a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts deleted file mode 100644 index c070547c967..00000000000 --- a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts +++ /dev/null @@ -1,162 +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/124024 @hediet @alexdima - - export namespace languages { - - /** - * Registers an inline completion provider. - * - * Multiple providers can be registered for a language. In that case providers are asked in - * parallel and the results are merged. A failing provider (rejected promise or exception) will - * not cause a failure of the whole operation. - * - * @param selector A selector that defines the documents this provider is applicable to. - * @param provider An inline completion provider. - * @return A {@link Disposable} that unregisters this provider when being disposed. - */ - export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; - } - - /** - * The inline completion item provider interface defines the contract between extensions and - * the inline completion feature. - * - * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. - */ - export interface InlineCompletionItemProvider { - - /** - * 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 explicitly asks for the next or previous inline completion. - * In that case, all available inline completions should be returned. - * `context.triggerKind` can be used to distinguish between these scenarios. - * - * @param document The document inline completions are requested for. - * @param position The position inline completions are requested for. - * @param context A context object with additional information. - * @param token A cancellation token. - * @return An array of completion items or a thenable that resolves to an array of completion items. - */ - provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; - } - - /** - * Represents a collection of {@link InlineCompletionItem inline completion items} to be presented - * in the editor. - */ - export class InlineCompletionList { - /** - * The inline completion items. - */ - items: InlineCompletionItem[]; - - /** - * Creates a new list of inline completion items. - */ - constructor(items: InlineCompletionItem[]); - } - - /** - * Provides information about the context in which an inline completion was requested. - */ - export interface InlineCompletionContext { - /** - * Describes how the inline completion was triggered. - */ - readonly triggerKind: InlineCompletionTriggerKind; - - /** - * 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. - */ - readonly selectedCompletionInfo: SelectedCompletionInfo | undefined; - } - - /** - * Describes the currently selected completion item. - */ - export interface SelectedCompletionInfo { - /** - * The range that will be replaced if this completion item is accepted. - */ - readonly range: Range; - - /** - * The text the range will be replaced with if this completion is accepted. - */ - readonly text: string; - } - - /** - * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. - */ - export enum InlineCompletionTriggerKind { - /** - * 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, - } - - /** - * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. - * - * @see {@link InlineCompletionItemProvider.provideInlineCompletionItems} - */ - export class InlineCompletionItem { - /** - * The text to replace the range with. Must be set. - * Is used both for the preview and the accept operation. - */ - insertText: string | SnippetString; - - /** - * A text that is used to decide if this inline completion should be shown. When `falsy` - * the {@link InlineCompletionItem.insertText} is used. - * - * An inline completion is shown if the text to replace is a prefix of the filter text. - */ - filterText?: string; - - /** - * The range to replace. - * Must begin and end on the same line. - * - * Prefer replacements over insertions to provide a better experience when the user deletes typed text. - */ - range?: Range; - - /** - * An optional {@link Command} that is executed *after* inserting this completion. - */ - command?: Command; - - /** - * Creates a new inline completion item. - * - * @param insertText The text to replace the range with. - * @param range The range to replace. If not set, the word at the requested position will be used. - * @param command An optional {@link Command} that is executed *after* inserting this completion. - */ - constructor(insertText: string | SnippetString, range?: Range, command?: Command); - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts b/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts index d30fdb167f4..95963aeaea5 100644 --- a/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts @@ -5,8 +5,9 @@ declare module 'vscode' { - // https://github.com/microsoft/vscode/issues/106744 + // https://github.com/microsoft/vscode/issues/147248 + /** @deprecated */ interface NotebookDocumentBackup { /** * Unique identifier for the backup. @@ -24,10 +25,12 @@ declare module 'vscode' { delete(): void; } + /** @deprecated */ interface NotebookDocumentBackupContext { readonly destination: Uri; } + /** @deprecated */ interface NotebookDocumentOpenContext { readonly backupId?: string; readonly untitledDocumentData?: Uint8Array; @@ -35,6 +38,8 @@ declare module 'vscode' { // todo@API use openNotebookDOCUMENT to align with openCustomDocument etc? // todo@API rename to NotebookDocumentContentProvider + /** @deprecated */ + export interface NotebookContentProvider { readonly options?: NotebookDocumentContentOptions; @@ -60,6 +65,7 @@ declare module 'vscode' { // TODO@api use NotebookDocumentFilter instead of just notebookType:string? // TODO@API options duplicates the more powerful variant on NotebookContentProvider + /** @deprecated */ export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions): Disposable; } } diff --git a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts index fd1d91db119..af681fc77ee 100644 --- a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts @@ -7,35 +7,9 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/149271 - /** - * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. - */ - export enum NotebookEditorRevealType { - /** - * The range will be revealed with as little scrolling as possible. - */ - Default = 0, + // ❗️ Important: The main NotebookEditor api has been finalized. + // This file only contains deprecated properties/functions from the proposal. - /** - * The range will always be revealed in the center of the viewport. - */ - InCenter = 1, - - /** - * If the range is outside the viewport, it will be revealed in the center of the viewport. - * Otherwise, it will be revealed with as little scrolling as possible. - */ - InCenterIfOutsideViewport = 2, - - /** - * The range will always be revealed at the top of the viewport. - */ - AtTop = 3 - } - - /** - * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. - */ export interface NotebookEditor { /** * The document associated with this notebook editor. @@ -43,152 +17,9 @@ declare module 'vscode' { * @deprecated Use {@linkcode NotebookEditor.notebook} instead. */ readonly document: NotebookDocument; - - /** - * The {@link NotebookDocument notebook document} associated with this notebook editor. - */ - readonly notebook: NotebookDocument; - - /** - * The primary selection in this notebook editor. - */ - selection: NotebookRange; - - /** - * All selections in this notebook editor. - * - * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; - */ - selections: readonly NotebookRange[]; - - /** - * The current visible ranges in the editor (vertically). - */ - readonly visibleRanges: readonly NotebookRange[]; - - /** - * The column in which this editor shows. - */ - readonly viewColumn?: ViewColumn; - - /** - * Scroll as indicated by `revealType` in order to reveal the given range. - * - * @param range A range. - * @param revealType The scrolling strategy for revealing `range`. - */ - revealRange(range: NotebookRange, revealType?: NotebookEditorRevealType): void; - } - - /** - * Represents an event describing the change in a {@link NotebookEditor.selections notebook editor's selections}. - */ - export interface NotebookEditorSelectionChangeEvent { - /** - * The {@link NotebookEditor notebook editor} for which the selections have changed. - */ - readonly notebookEditor: NotebookEditor; - - /** - * The new value for the {@link NotebookEditor.selections notebook editor's selections}. - */ - readonly selections: readonly NotebookRange[]; - } - - /** - * Represents an event describing the change in a {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. - */ - export interface NotebookEditorVisibleRangesChangeEvent { - /** - * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. - */ - readonly notebookEditor: NotebookEditor; - - /** - * The new value for the {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. - */ - readonly visibleRanges: readonly NotebookRange[]; - } - - /** - * Represents options to configure the behavior of showing a {@link NotebookDocument notebook document} in an {@link NotebookEditor notebook editor}. - */ - export interface NotebookDocumentShowOptions { - /** - * An optional view column in which the {@link NotebookEditor notebook editor} should be shown. - * The default is the {@link ViewColumn.Active active}, other values are adjusted to - * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is - * not adjusted. Use {@linkcode ViewColumn.Beside} to open the - * editor to the side of the currently active one. - */ - readonly viewColumn?: ViewColumn; - - /** - * An optional flag that when `true` will stop the {@link NotebookEditor notebook editor} from taking focus. - */ - readonly preserveFocus?: boolean; - - /** - * An optional flag that controls if an {@link NotebookEditor notebook editor}-tab shows as preview. Preview tabs will - * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends - * on the `workbench.editor.enablePreview`-setting. - */ - readonly preview?: boolean; - - /** - * An optional selection to apply for the document in the {@link NotebookEditor notebook editor}. - */ - readonly selections?: readonly NotebookRange[]; } export namespace window { - /** - * The currently visible {@link NotebookEditor notebook editors} or an empty array. - */ - export const visibleNotebookEditors: readonly NotebookEditor[]; - - /** - * An {@link Event} which fires when the {@link window.visibleNotebookEditors visible notebook editors} - * has changed. - */ - export const onDidChangeVisibleNotebookEditors: Event; - - /** - * The currently active {@link NotebookEditor notebook editor} or `undefined`. The active editor is the one - * that currently has focus or, when none has focus, the one that has changed - * input most recently. - */ - export const activeNotebookEditor: NotebookEditor | undefined; - - /** - * An {@link Event} which fires when the {@link window.activeNotebookEditor active notebook editor} - * has changed. *Note* that the event also fires when the active editor changes - * to `undefined`. - */ - export const onDidChangeActiveNotebookEditor: Event; - - /** - * An {@link Event} which fires when the {@link NotebookEditor.selections notebook editor selections} - * have changed. - */ - export const onDidChangeNotebookEditorSelection: Event; - - /** - * An {@link Event} which fires when the {@link NotebookEditor.visibleRanges notebook editor visible ranges} - * have changed. - */ - export const onDidChangeNotebookEditorVisibleRanges: Event; - - /** - * Show the given {@link NotebookDocument} in a {@link NotebookEditor notebook editor}. - * - * @param document A text document to be shown. - * @param options {@link NotebookDocumentShowOptions Editor options} to configure the behavior of showing the {@link NotebookEditor notebook editor}. - * - * @return A promise that resolves to an {@link NotebookEditor notebook editor}. - */ - export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; - /** * A short-hand for `openNotebookDocument(uri).then(document => showNotebookDocument(document, options))`. * diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.css b/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts similarity index 87% rename from src/vs/platform/contextview/browser/contextMenuHandler.css rename to src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts index 51a9e400923..a0f2c9e2df8 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.css +++ b/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts @@ -3,7 +3,5 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.context-view .monaco-menu { - min-width: 130px; +declare module 'vscode' { } - diff --git a/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts b/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts deleted file mode 100644 index 07f8e833f15..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts +++ /dev/null @@ -1,56 +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' { - - - export interface NotebookProxyController { - /** - * The identifier of this notebook controller. - * - * _Note_ that controllers are remembered by their identifier and that extensions should use - * stable identifiers across sessions. - */ - readonly id: string; - - /** - * The notebook type this controller is for. - */ - readonly notebookType: string; - - /** - * The human-readable label of this notebook controller. - */ - label: string; - - /** - * The human-readable description which is rendered less prominent. - */ - description?: string; - - /** - * The human-readable detail which is rendered less prominent. - */ - detail?: string; - - /** - * The human-readable label used to categorise controllers. - */ - kind?: string; - - resolveHandler: () => NotebookController | string | Thenable; - - readonly onDidChangeSelectedNotebooks: Event<{ readonly notebook: NotebookDocument; readonly selected: boolean }>; - - /** - * Dispose and free associated resources. - */ - dispose(): void; - } - - export namespace notebooks { - export function createNotebookProxyController(id: string, notebookType: string, label: string, resolveHandler: () => NotebookController | string | Thenable): NotebookProxyController; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts b/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts index 01330ec9cdd..14965fbe054 100644 --- a/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts @@ -21,44 +21,59 @@ declare module 'vscode' { static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit; /** - * Utility to create a edit that deletes cells in a notebook. + * Utility to create an edit that replaces cells in a notebook. + * + * @param index The index to insert cells at. + * @param newCells The new notebook cells. + */ + static insertCells(index: number, newCells: NotebookCellData[]): NotebookEdit; + + /** + * Utility to create an edit that deletes cells in a notebook. * * @param range The range of cells to delete. */ static deleteCells(range: NotebookRange): NotebookEdit; /** - * Utility to update a cells metadata. + * Utility to create an edit that update a cell's metadata. * * @param index The index of the cell to update. - * @param newMetadata The new metadata for the cell. + * @param newCellMetadata The new metadata for the cell. */ - static updateCellMetadata(index: number, newMetadata: { [key: string]: any }): NotebookEdit; + static updateCellMetadata(index: number, newCellMetadata: { [key: string]: any }): NotebookEdit; /** - * Range of the cells being edited + * Utility to create an edit that updates the notebook's metadata. + * + * @param newNotebookMetadata The new metadata for the notebook. */ - readonly range: NotebookRange; + static updateNotebookMetadata(newNotebookMetadata: { [key: string]: any }): NotebookEdit; + + /** + * Range of the cells being edited. May be empty. + */ + range: NotebookRange; /** * New cells being inserted. May be empty. */ - readonly newCells: NotebookCellData[]; + newCells: NotebookCellData[]; /** * Optional new metadata for the cells. */ - readonly newCellMetadata?: { [key: string]: any }; + newCellMetadata?: { [key: string]: any }; - constructor(range: NotebookRange, newCells: NotebookCellData[], newCellMetadata?: { [key: string]: any }); + /** + * Optional new metadata for the notebook. + */ + newNotebookMetadata?: { [key: string]: any }; + + constructor(range: NotebookRange, newCells: NotebookCellData[]); } export interface WorkspaceEdit { - /** - * Replaces the metadata for a notebook document. - */ - replaceNotebookMetadata(uri: Uri, value: { [key: string]: any }): void; - /** * Set (and replace) edits for a resource. * diff --git a/src/vs/editor/common/dnd.ts b/src/vscode-dts/vscode.proposed.scmInput.d.ts similarity index 51% rename from src/vs/editor/common/dnd.ts rename to src/vscode-dts/vscode.proposed.scmInput.d.ts index 748362a176a..6efdb57ae45 100644 --- a/src/vs/editor/common/dnd.ts +++ b/src/vscode-dts/vscode.proposed.scmInput.d.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; +declare module 'vscode' { -export interface IDataTransferFile { - readonly name: string; - readonly uri?: URI; - data(): Promise; + // https://github.com/microsoft/vscode/issues/150268 + + /** + * Represents the input box in the Source Control viewlet. + */ + export interface SourceControlInputBox { + + /** + * Controls whether the input box is enabled (default is `true`). + */ + enabled: boolean; + } } - -export interface IDataTransferItem { - asString(): Thenable; - asFile(): IDataTransferFile | undefined; - value: any; -} - -export type IDataTransfer = Map; diff --git a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts index 879e5a757b7..5468bc218cd 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts @@ -18,7 +18,7 @@ declare module 'vscode' { * * The user can drop into a text editor by holding down `shift` while dragging. Requires `workbench.experimental.editor.dropIntoEditor.enabled` to be on. */ - export interface DocumentOnDropProvider { + export interface DocumentOnDropEditProvider { /** * Provide edits which inserts the content being dragged and dropped into the document. * @@ -35,13 +35,13 @@ declare module 'vscode' { export namespace languages { /** - * Registers a new {@link DocumentOnDropProvider}. + * Registers a new {@link DocumentOnDropEditProvider}. * * @param selector A selector that defines the documents this provider applies to. * @param provider A drop provider. * * @return A {@link Disposable} that unregisters this provider when disposed of. */ - export function registerDocumentOnDropProvider(selector: DocumentSelector, provider: DocumentOnDropProvider): Disposable; + export function registerDocumentOnDropEditProvider(selector: DocumentSelector, provider: DocumentOnDropEditProvider): Disposable; } } diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 18350685d5e..39a996b6ff4 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","181b43c0e2949e36ecb623d8cc6de29d4fa2bae8"],["skipWelcome","true"]]`; + const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","3c8520fab514b9f56070214496b26ff68d1b1cb5"],["skipWelcome","true"]]`; if (path.extname(testWorkspacePath) === '.code-workspace') { await page.goto(`${endpoint.href}&workspace=${testWorkspacePath}&payload=${payloadParam}`); diff --git a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts index b94b40045f7..58f861d2b65 100644 --- a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts +++ b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Application, Terminal, SettingsEditor } from '../../../../automation'; +import { Application, Terminal, SettingsEditor, TerminalCommandIdWithValue } from '../../../../automation'; import { setTerminalTestSettings } from './terminal-helpers'; export function setup() { @@ -24,28 +24,31 @@ export function setup() { await settingsEditor.clearUserSettings(); }); + async function createShellIntegrationProfile() { + await terminal.runCommandWithValue(TerminalCommandIdWithValue.NewWithProfile, process.platform === 'win32' ? 'PowerShell' : 'bash'); + } + describe('Shell integration', function () { - // TODO: Fix on Linux, some distros use sh as the default shell in which case shell integration will fail - (process.platform === 'win32' || process.platform === 'linux' ? describe.skip : describe)('Decorations', function () { + (process.platform === 'linux' || process.platform === 'win32' ? describe.skip : describe)('Decorations', function () { describe('Should show default icons', function () { it('Placeholder', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); }); it('Success', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.runCommandInTerminal(`ls`); await terminal.assertCommandDecorations({ placeholder: 1, success: 1, error: 0 }); }); it('Error', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.runCommandInTerminal(`fsdkfsjdlfksjdkf`); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 1 }); }); }); describe('Custom configuration', function () { it('Should update and show custom icons', async () => { - await terminal.createTerminal(); + await createShellIntegrationProfile(); await terminal.assertCommandDecorations({ placeholder: 1, success: 0, error: 0 }); await terminal.runCommandInTerminal(`ls`); await terminal.runCommandInTerminal(`fsdkfsjdlfksjdkf`); diff --git a/test/smoke/src/areas/terminal/terminal-tabs.test.ts b/test/smoke/src/areas/terminal/terminal-tabs.test.ts index 1f962540d75..cc4573978e8 100644 --- a/test/smoke/src/areas/terminal/terminal-tabs.test.ts +++ b/test/smoke/src/areas/terminal/terminal-tabs.test.ts @@ -67,7 +67,7 @@ export function setup() { await terminal.assertSingleTab({ name }); }); - it.skip('should reset the tab name to the default value when no name is provided', async () => { // https://github.com/microsoft/vscode/issues/146796 + it('should reset the tab name to the default value when no name is provided', async () => { await terminal.createTerminal(); const defaultName = await terminal.getSingleTabName(); const name = 'my terminal name'; diff --git a/test/smoke/src/areas/workbench/data-loss.test.ts b/test/smoke/src/areas/workbench/data-loss.test.ts index 24939826c43..b08ea31ae5f 100644 --- a/test/smoke/src/areas/workbench/data-loss.test.ts +++ b/test/smoke/src/areas/workbench/data-loss.test.ts @@ -127,7 +127,7 @@ export function setup(ensureStableCode: () => string | undefined, logger: Logger } }); - describe.skip('Data Loss (stable -> insiders)', () => { //TODO@bpasero enable again once we shipped 1.67.x + describe('Data Loss (stable -> insiders)', () => { let insidersApp: Application | undefined = undefined; let stableApp: Application | undefined = undefined; diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index be81bb17d4c..a6c86ba20d1 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -9,11 +9,11 @@ import { installAllHandlers } from '../../utils'; export function setup(logger: Logger) { describe('Localization', () => { - // Shared before/after handling installAllHandlers(logger); - it('starts with "DE" locale and verifies title and viewlets text is in German', async function () { + // skipped until translations are available https://github.com/microsoft/vscode/issues/150324 + it.skip('starts with "DE" locale and verifies title and viewlets text is in German', async function () { const app = this.app as Application; await app.workbench.extensions.openExtensionsViewlet(); diff --git a/yarn.lock b/yarn.lock index 8b7a2c516e6..1125a522c4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2563,16 +2563,7 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" -bl@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" - integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bl@^4.0.3: +bl@^4.0.2, bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -3957,9 +3948,9 @@ decompress@^4.2.1: strip-dirs "^2.0.0" deemon@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/deemon/-/deemon-1.4.0.tgz#01c09cc23eec41e5d7ddac082eb52c3611d38dff" - integrity sha512-S0zK5tNTdVFsJZVUeKi/CYJn4zzhW0Y55lwXzv2hVxb7ajzAHf91BhE5y2xvx1X7czIZ6PHLPDj00TVAmylVXw== + version "1.7.1" + resolved "https://registry.yarnpkg.com/deemon/-/deemon-1.7.1.tgz#46cf8313fd320fac6ee944bbb1bea3e8c2a978ee" + integrity sha512-UcBiu6+4sZgkDrs7GT78FDkqmt9ZVN412XXT4Bm9sn5Hcrwv/M9HmCay3108Yvxl4Ti5n1UjkkuSVA1y3+FIvg== dependencies: bl "^4.0.2" tree-kill "^1.2.2" @@ -4296,10 +4287,10 @@ 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@17.4.3: - version "17.4.3" - resolved "https://registry.yarnpkg.com/electron/-/electron-17.4.3.tgz#5f3c26cb211f9267d2becee717f34e3ce564a6bf" - integrity sha512-WQggyCgNUOzoOn+wJKe+xFhYy56gyrn/jIa/l7dyD3TxPb8lddSc86OAqPnP5EugcNXQ0yIu8b+SIE8duKozSw== +electron@17.4.4: + version "17.4.4" + resolved "https://registry.yarnpkg.com/electron/-/electron-17.4.4.tgz#a289fa5cff6a59ef83647517a295eca780d64a86" + integrity sha512-/CqXJwm1VLfhF7+QhCrPEoePcpGMdRh09A+sVHX+kgT1twrmNH8S+ZeMPYxX8EU0O0Eki3UfA5zA2ADWaCDq2Q== dependencies: "@electron/get" "^1.13.0" "@types/node" "^14.6.2" @@ -8136,6 +8127,11 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-addon-api@*: + version "5.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" + integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== + 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" @@ -11003,10 +10999,10 @@ tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" -tas-client-umd@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.5.tgz#743c02e344afdec55a68bb9d62805e30a1ae83d4" - integrity sha512-NL9eFzYBBHfiYja6tP27084j4YbqtGEk68C5BTyTNHapsM9dizZ/RoSUGst5L1xUiw1zO1WbHf4Lir2e/wgT8g== +tas-client-umd@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.6.tgz#a0cf70a68f50d406773457630666224f0eb545a6" + integrity sha512-eOz5IK4cuNmSZI9QlqlT0FdvgfnnHDB6rjqleFaYAbzYE4RdJzYNiM28zFIXgmOVEgESvfabMFxG8WX5M4z3HA== temp@^0.8.3: version "0.8.4" @@ -11394,10 +11390,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.8.0-dev.20220511: - version "4.8.0-dev.20220511" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.0-dev.20220511.tgz#42dd5ad99dcd345277c784949cc36a7b5d43fa18" - integrity sha512-MDo0tI/TRHCJ1sxochoU4LhT41C6jhEIkWneehqsxNYV84+d+C4HvD7Nq4DQrI03cIGvPJbdW2s3ZladROXE+A== +typescript@^4.8.0-dev.20220518: + version "4.8.0-dev.20220518" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.0-dev.20220518.tgz#3082c89c764daece904480552b9f2c3f5ce79c56" + integrity sha512-yczRLiowXD4THxpe2DrClYXsmIRt9VPDft1dat4Le50mQwuUcmvdqD43o4hmlbNP7HpyeEYX51KXozGq1s7zlw== typical@^4.0.0: version "4.0.0" @@ -11791,6 +11787,14 @@ vscode-oniguruma@1.6.1: resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.1.tgz#2bf4dfcfe3dd2e56eb549a3068c8ee39e6c30ce5" integrity sha512-vc4WhSIaVpgJ0jJIejjYxPvURJavX6QG41vu0mGhqywMkQqulezEqEQ3cO3gc8GvcOpX6ycmKGqRoROEMBNXTQ== +vscode-policy-watcher@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vscode-policy-watcher/-/vscode-policy-watcher-1.1.0.tgz#2921353c5080b3452929f1e350b9fab9ff852cc9" + integrity sha512-yPvy3Or66H0l8/FyWbJeGxpWW3GDZf65EIT7fqp1Ethdz4ecEnHThuHti7SlfdRJTf5qnifrX7af02INiHqDMA== + dependencies: + bindings "^1.5.0" + node-addon-api "*" + vscode-proxy-agent@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/vscode-proxy-agent/-/vscode-proxy-agent-0.12.0.tgz#0775f464b9519b0c903da4dcf50851e1453f4e48" @@ -12194,10 +12198,10 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-search@0.9.0-beta.35: - version "0.9.0-beta.35" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.35.tgz#524ee3be855c1e8db234c6795bdb44bb6baff8fd" - integrity sha512-hTDqAhqlhBvz3dtdK1Tg5Al2U3HquSHpV1xCX+bbOmbgprAxUrSQxslUPDD69CTazzTyif3L19M08hccRyr1Ug== +xterm-addon-search@0.9.0-beta.39: + version "0.9.0-beta.39" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.39.tgz#e8376e1485ee7d763c07d1a8f1354114f65b3e3e" + integrity sha512-h45wkecgfqXXoAUqgNytAfSd6g0xNT6rZy/enVaEU0aes7QoL9pxHUKkCry8PP6hs03Slk0VxQ4AGsbSZGvK/w== xterm-addon-serialize@0.7.0-beta.12: version "0.7.0-beta.12" @@ -12209,20 +12213,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.33: - version "0.12.0-beta.33" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.33.tgz#cb539db9e41f06087b692f0f42491a73bc4bd013" - integrity sha512-seOm06exR36U0/EvR/CUNGuy99RAndoyWEdXg6S16rgEZ4G2Yj9iov/QdCtc4gwq9hFzVETFPlDW+Ge8xeHIzA== +xterm-addon-webgl@0.12.0-beta.36: + version "0.12.0-beta.36" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.36.tgz#460f80829a78c979a448d5b764699af3f0366ff1" + integrity sha512-sgX7OHSGZQZE5b4xtPqd/5NEcll0Z+00tnTVxKZlXf5XEENcG0tnBF4I4f+k9K3cmjE1UIUVG2yYPrqWlYCdpA== -xterm-headless@4.19.0-beta.41: - version "4.19.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.41.tgz#f495ff173c7952aafa0c785acf15f20942c6fdc7" - integrity sha512-j09IFsM4tBSpjgY5OQSB1llojwEGyFFxgD36MYXZtopmB8p9+0l5GFq5hYfJojGfHCNaB/RwWAexGUxBK2ABRA== +xterm-headless@4.19.0-beta.56: + version "4.19.0-beta.56" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.56.tgz#7e6bdc8d647916bf5de64a73eee6bd508d25e344" + integrity sha512-EZoR/HqZoernhFngFQp7gUPy+G0TpEJkbJ9HVZcINC3m8wuV1wZKfZ4xBhsRPfhSJ7rsPnqbC+qez5ZjxwYEIw== -xterm@4.19.0-beta.41: - version "4.19.0-beta.41" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.41.tgz#acb6009028898e9cfac41d4aa2865f81f6f56c5f" - integrity sha512-WY1NuxF/yUVN3l0TgzQGjrGM26eOu5g0Dbfam8GCkgdK5yrsgPF0xwM7UEj8sDjp5FbxEkSm//X86IIsgzqqFw== +xterm@4.19.0-beta.56: + version "4.19.0-beta.56" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.56.tgz#a3f1021b43ac04aa0c3f7b06f1b44ad34d487879" + integrity sha512-kywKIK61oPjbloZI+jXY1zgjQm/ghOsFFMjb79IIMaWocUDDqdpo9MmGwziTVZYu4w/Air2Zfas9UWBu4/KEyA== y18n@^3.2.1: version "3.2.2"