diff --git a/.github/workflows/deep-classifier-assign-monitor.yml b/.github/workflows/deep-classifier-assign-monitor.yml index 9d74e308472..97e375694e6 100644 --- a/.github/workflows/deep-classifier-assign-monitor.yml +++ b/.github/workflows/deep-classifier-assign-monitor.yml @@ -19,6 +19,6 @@ jobs: - name: "Run Classifier: Monitor" uses: ./actions/classifier-deep/monitor with: - botName: vscode-triage-bot + botName: VSCodeTriageBot token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/deep-classifier-unassign-monitor.yml b/.github/workflows/deep-classifier-unassign-monitor.yml index 35505a4015e..6e9a2b13621 100644 --- a/.github/workflows/deep-classifier-unassign-monitor.yml +++ b/.github/workflows/deep-classifier-unassign-monitor.yml @@ -19,6 +19,6 @@ jobs: - name: "Run Classifier: Monitor" uses: ./actions/classifier-deep/monitor with: - botName: vscode-triage-bot + botName: VSCodeTriageBot token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index b2e93e24a7c..af70c86caa0 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -16,29 +16,23 @@ jobs: - name: Install Actions run: npm install --production --prefix ./actions - - name: Run CopyCat (JacksonKearl/testissues) + - name: Run CopyCat (VSCodeTriageBot/testissues) uses: ./actions/copycat with: appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - owner: JacksonKearl + owner: VSCodeTriageBot repo: testissues - # - name: Run CopyCat (chrmarti/testissues) - # uses: ./actions/copycat - # with: - # appInsightsKey: ${{secrets.TRIAGE_ACTIONS_APP_INSIGHTS}} - # token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - # owner: chrmarti - # repo: testissues - - name: Run New Release 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 fdbbd0cf04b..0c8d6bab793 100644 --- a/.github/workflows/pr-chat.yml +++ b/.github/workflows/pr-chat.yml @@ -1,7 +1,7 @@ name: PR Chat on: - pull_request: - types: [opened, ready_for_review] + pull_request_target: + types: [opened, ready_for_review, closed] jobs: main: @@ -20,4 +20,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/settings.json b/.vscode/settings.json index d7996ff169e..e698d02574e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,6 +67,11 @@ } ], "git.ignoreLimitWarning": true, + "git.branchProtection": [ + "main", + "release/*" + ], + "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "git.branchRandomName.enable": true, "remote.extensionKind": { "msjsdiag.debugger-for-chrome": "workspace" 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..4e6f8f13d0e 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 @@ -221,104 +230,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..5b09f8ee77e 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: @@ -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/product-build.yml b/build/azure-pipelines/product-build.yml index f0f9ab8e2f6..3cc4fc399d9 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -221,7 +221,7 @@ stages: parameters: VSCODE_QUALITY: ${{ variables.VSCODE_QUALITY }} - - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(parameters.VSCODE_BUILD_LINUX_ARM64, true)) }}: - job: arm64 variables: VSCODE_ARCH: arm64 diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index d089f70d228..286283a31dc 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 @@ -184,105 +193,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/extensions/git/package.json b/extensions/git/package.json index db9c80d4718..de341a095ee 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -459,6 +459,21 @@ "title": "%command.revealInExplorer%", "category": "Git" }, + { + "command": "git.revealFileInOS.linux", + "title": "%command.revealFileInOS.linux%", + "category": "Git" + }, + { + "command": "git.revealFileInOS.mac", + "title": "%command.revealFileInOS.mac%", + "category": "Git" + }, + { + "command": "git.revealFileInOS.windows", + "title": "%command.revealFileInOS.windows%", + "category": "Git" + }, { "command": "git.stashIncludeUntracked", "title": "%command.stashIncludeUntracked%", @@ -761,6 +776,18 @@ "command": "git.revealInExplorer", "when": "false" }, + { + "command": "git.revealFileInOS.linux", + "when": "false" + }, + { + "command": "git.revealFileInOS.mac", + "when": "false" + }, + { + "command": "git.revealFileInOS.windows", + "when": "false" + }, { "command": "git.undoCommit", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1211,7 +1238,22 @@ { "command": "git.revealInExplorer", "when": "scmProvider == git && scmResourceGroup == merge", - "group": "2_view" + "group": "2_view@1" + }, + { + "command": "git.revealFileInOS.linux", + "when": "scmProvider == git && scmResourceGroup == merge && isLinux", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.mac", + "when": "scmProvider == git && scmResourceGroup == merge && isMac", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.windows", + "when": "scmProvider == git && scmResourceGroup == merge && isWindows", + "group": "2_view@2" }, { "command": "git.openFile2", @@ -1251,7 +1293,22 @@ { "command": "git.revealInExplorer", "when": "scmProvider == git && scmResourceGroup == index", - "group": "2_view" + "group": "2_view@1" + }, + { + "command": "git.revealFileInOS.linux", + "when": "scmProvider == git && scmResourceGroup == index && isLinux", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.mac", + "when": "scmProvider == git && scmResourceGroup == index && isMac", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.windows", + "when": "scmProvider == git && scmResourceGroup == index && isWindows", + "group": "2_view@2" }, { "command": "git.openFile2", @@ -1316,7 +1373,22 @@ { "command": "git.revealInExplorer", "when": "scmProvider == git && scmResourceGroup == workingTree", - "group": "2_view" + "group": "2_view@1" + }, + { + "command": "git.revealFileInOS.linux", + "when": "scmProvider == git && scmResourceGroup == workingTree && isLinux", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.mac", + "when": "scmProvider == git && scmResourceGroup == workingTree && isMac", + "group": "2_view@2" + }, + { + "command": "git.revealFileInOS.windows", + "when": "scmProvider == git && scmResourceGroup == workingTree && isWindows", + "group": "2_view@2" }, { "command": "git.openChange", @@ -1780,7 +1852,33 @@ "git.branchPrefix": { "type": "string", "description": "%config.branchPrefix%", - "default": "" + "default": "", + "scope": "resource" + }, + "git.branchProtection": { + "type": "array", + "markdownDescription": "%config.branchProtection%", + "items": { + "type": "string" + }, + "default": [], + "scope": "resource" + }, + "git.branchProtectionPrompt": { + "type": "string", + "description": "%config.branchProtectionPrompt%", + "enum": [ + "alwaysCommit", + "alwaysCommitToNewBranch", + "alwaysPrompt" + ], + "enumDescriptions": [ + "%config.branchProtectionPrompt.alwaysCommit%", + "%config.branchProtectionPrompt.alwaysCommitToNewBranch%", + "%config.branchProtectionPrompt.alwaysPrompt%" + ], + "default": "alwaysPrompt", + "scope": "resource" }, "git.branchValidationRegex": { "type": "string", @@ -1795,7 +1893,8 @@ "git.branchRandomName.enable": { "type": "boolean", "description": "%config.branchRandomNameEnable%", - "default": false + "default": false, + "scope": "resource" }, "git.branchRandomName.dictionary": { "type": "array", @@ -1806,7 +1905,8 @@ "default": [ "adjectives", "animals" - ] + ], + "scope": "resource" }, "git.confirmSync": { "type": "boolean", @@ -2530,6 +2630,7 @@ "byline": "^5.0.0", "file-type": "^7.2.0", "jschardet": "3.0.0", + "picomatch": "2.3.1", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", "which": "^1.3.0" @@ -2539,6 +2640,7 @@ "@types/file-type": "^5.2.1", "@types/mocha": "^9.1.1", "@types/node": "16.x", + "@types/picomatch": "2.3.0", "@types/which": "^1.0.28" }, "repository": { diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 10c030fb034..ec691130508 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -80,6 +80,9 @@ "command.showOutput": "Show Git Output", "command.ignore": "Add to .gitignore", "command.revealInExplorer": "Reveal in Explorer View", + "command.revealFileInOS.linux": "Open Containing Folder", + "command.revealFileInOS.mac": "Reveal in Finder", + "command.revealFileInOS.windows": "Reveal in File Explorer", "command.rebaseAbort": "Abort Rebase", "command.stashIncludeUntracked": "Stash (Include Untracked)", "command.stash": "Stash", @@ -117,6 +120,11 @@ "config.checkoutType.tags": "Tags", "config.checkoutType.remote": "Remote branches", "config.branchPrefix": "Prefix used when creating a new branch.", + "config.branchProtection": "List of protected branches. By default, a prompt is shown before changes are committed to a protected branch. The prompt can be controlled using the `#git.branchProtectionPrompt#` setting.", + "config.branchProtectionPrompt": "Controls whether a prompt is being before changes are committed to a protected branch.", + "config.branchProtectionPrompt.alwaysCommit": "Always commit changes to the protected branch.", + "config.branchProtectionPrompt.alwaysCommitToNewBranch": "Always commit changes to a new branch.", + "config.branchProtectionPrompt.alwaysPrompt": "Always prompt before changes are committed to a protected branch.", "config.branchRandomNameDictionary": "List of dictionaries used when the branch name that is randomly generated. Supported values: `adjectives`, `animals`, `colors`, `numbers`.", "config.branchRandomNameEnable": "Controls whether a random name is generated when creating a new branch.", "config.branchValidationRegex": "A regular expression to validate new branch names.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 790f21439eb..a3199cffc3a 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -5,6 +5,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 TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; @@ -1496,6 +1497,36 @@ export class CommandCenter { opts.all = 'tracked'; } + // Branch protection + const branchProtection = config.get('branchProtection')!.map(bp => bp.trim()).filter(bp => bp !== ''); + const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!; + const branchIsProtected = branchProtection.some(bp => picomatch.isMatch(repository.HEAD?.name ?? '', bp)); + + if (branchIsProtected && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) { + const commitToNewBranch = localize('commit to branch', "Commit to a New Branch"); + + let pick: string | undefined = commitToNewBranch; + + if (branchProtectionPrompt === 'alwaysPrompt') { + const message = localize('confirm branch protection commit', "You are trying to commit to a protected branch and you might not have permission to push your commits to the remote.\n\nHow would you like to proceed?"); + const commit = localize('commit changes', "Commit Anyway"); + + pick = await window.showWarningMessage(message, { modal: true }, commitToNewBranch, commit); + } + + if (!pick) { + return false; + } else if (pick === commitToNewBranch) { + const branchName = await this.promptForBranchName(repository); + + if (!branchName) { + return false; + } + + await repository.branch(branchName, true); + } + } + await repository.commit(message, opts); const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand'); @@ -2546,6 +2577,21 @@ export class CommandCenter { await commands.executeCommand('revealInExplorer', resourceState.resourceUri); } + @command('git.revealFileInOS.linux') + @command('git.revealFileInOS.mac') + @command('git.revealFileInOS.windows') + async revealFileInOS(resourceState: SourceControlResourceState): Promise { + if (!resourceState) { + return; + } + + if (!(resourceState.resourceUri instanceof Uri)) { + return; + } + + await commands.executeCommand('revealFileInOS', resourceState.resourceUri); + } + private async _stash(repository: Repository, includeUntracked = false): Promise { const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0 && (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 24768c1dba4..0327aae4868 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -67,6 +67,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu const terminalEnvironmentManager = new TerminalEnvironmentManager(context, environment); disposables.push(terminalEnvironmentManager); + outputChannelLogger.logInfo(localize('using git', "Using git {0} from {1}", info.version, info.path)); const git = new Git({ gitPath: info.path, @@ -82,8 +83,6 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu model.onDidCloseRepository(onRepository, null, disposables); onRepository(); - outputChannelLogger.logInfo(localize('using git', "Using git {0} from {1}", info.version, info.path)); - const onOutput = (str: string) => { const lines = str.split(/\r?\n/mg); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index e6897562cc7..87c510b3b03 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -143,9 +143,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR private async scanWorkspaceFolders(): Promise { const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); - - // Log repository scan settings - this.outputChannelLogger.logTrace(`autoRepositoryDetection="${autoRepositoryDetection}"`); + this.outputChannelLogger.logTrace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`); if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') { return; @@ -153,6 +151,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR await Promise.all((workspace.workspaceFolders || []).map(async folder => { const root = folder.uri.fsPath; + this.outputChannelLogger.logTrace(`[swsf] Workspace folder: ${root}`); // Workspace folder children const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('repositoryScanMaxDepth', 1); @@ -162,19 +161,25 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR // Repository scan folders const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get('scanRepositories') || []; + this.outputChannelLogger.logTrace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`); + for (const scanPath of scanPaths) { if (scanPath === '.git') { + this.outputChannelLogger.logTrace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.'); continue; } if (path.isAbsolute(scanPath)) { - console.warn(localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.")); + const notSupportedMessage = localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting."); + this.outputChannelLogger.logWarning(notSupportedMessage); + console.warn(notSupportedMessage); continue; } subfolders.add(path.join(root, scanPath)); } + this.outputChannelLogger.logTrace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`); await Promise.all([...subfolders].map(f => this.openRepository(f))); })); } @@ -245,6 +250,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR .filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[]; openRepositoriesToDispose.forEach(r => r.dispose()); + this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath))); } @@ -258,17 +264,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR .filter(({ root }) => workspace.getConfiguration('git', root).get('enabled') !== true) .map(({ repository }) => repository); + this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`); possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath)); openRepositoriesToDispose.forEach(r => r.dispose()); } private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise { if (!workspace.isTrusted) { + this.outputChannelLogger.logTrace('[svte] Workspace is not trusted.'); return; } const config = workspace.getConfiguration('git'); const autoRepositoryDetection = config.get('autoRepositoryDetection'); + this.outputChannelLogger.logTrace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`); if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') { return; @@ -284,16 +293,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR const repository = this.getRepository(uri); if (repository) { + this.outputChannelLogger.logTrace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`); return; } + this.outputChannelLogger.logTrace(`[svte] Open repository for editor resource ${uri.fsPath}`); await this.openRepository(path.dirname(uri.fsPath)); })); } @sequentialize async openRepository(repoPath: string): Promise { + this.outputChannelLogger.logTrace(`Opening repository: ${repoPath}`); if (this.getRepository(repoPath)) { + this.outputChannelLogger.logTrace(`Repository for path ${repoPath} already exists`); return; } @@ -301,6 +314,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR const enabled = config.get('enabled') === true; if (!enabled) { + this.outputChannelLogger.logTrace('Git is not enabled'); return; } @@ -310,6 +324,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK); const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']); if (result.stderr.trim() === '' && result.stdout.trim() === '') { + this.outputChannelLogger.logTrace(`Bare repository: ${repoPath}`); return; } } catch { @@ -324,12 +339,15 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR // case insensitive file systems // https://github.com/microsoft/vscode/issues/33498 const repositoryRoot = Uri.file(rawRoot).fsPath; + this.outputChannelLogger.logTrace(`Repository root: ${repositoryRoot}`); if (this.getRepository(repositoryRoot)) { + this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} already exists`); return; } if (this.shouldRepositoryBeIgnored(rawRoot)) { + this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is ignored`); return; } @@ -345,6 +363,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR this.showRepoOnHomeDriveRootWarning = false; } + this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is on the root of the HOMEDRIVE`); return; } } diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index cfeee5c745e..60b91dbb320 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -36,6 +36,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== +"@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/which@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" @@ -71,6 +76,11 @@ jschardet@3.0.0: resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-3.0.0.tgz#898d2332e45ebabbdb6bf2feece9feea9a99e882" integrity sha512-lJH6tJ77V8Nzd5QWRkFYCLc13a3vADkh3r/Fi8HupZGWk2OVVDfnZP8V/VgQgZ+lzW0kG2UGb5hFgt3V3ndotQ== +picomatch@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + vscode-nls@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index 6713556ae38..b3df5fc8e51 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -321,15 +321,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); diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index c8feabc17af..32802630b84 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -9,8 +9,7 @@ "vscode": "^1.57.0" }, "enabledApiProposals": [ - "notebookEditor", - "notebookEditorEdit" + "notebookEditor" ], "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..918db6ea6c2 100644 --- a/extensions/ipynb/tsconfig.json +++ b/extensions/ipynb/tsconfig.json @@ -10,6 +10,6 @@ "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/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 9bf63477ebe..5246257adc8 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -242,7 +242,9 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { /* __GDPR__ "json.schema" : { - "schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + "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 }); diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 25aef750a76..4941fced265 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -147,7 +147,7 @@ ] }, "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" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 98216da36e6..7e56398f062 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" diff --git a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts index fd11cce6531..f53a83d670e 100644 --- a/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts +++ b/extensions/markdown-language-features/src/languageFeatures/diagnostics.ts @@ -12,7 +12,7 @@ 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'; const localize = nls.loadMessageBundle(); @@ -118,6 +118,94 @@ 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()); + } + } +} + export class DiagnosticManager extends Disposable { private readonly collection: vscode.DiagnosticCollection; @@ -126,6 +214,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 +238,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 +262,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 +278,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 +370,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 { diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts index e6fadaa05f2..2a239a04491 100644 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts +++ b/extensions/markdown-language-features/src/languageFeatures/documentLinkProvider.ts @@ -47,6 +47,11 @@ function parseLink( return { kind: 'external', uri: externalSchemeUri }; } + if (/^[a-z\-][a-z\-]+:/i.test(cleanLink)) { + // Looks like a uri + return { kind: 'external', uri: vscode.Uri.parse(cleanLink) }; + } + // Assume it must be an relative or absolute file path // Use a fake scheme to avoid parse warnings const tempUri = vscode.Uri.parse(`vscode-resource:${link}`); @@ -182,35 +187,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 { @@ -257,35 +265,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)) { @@ -296,7 +304,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 { @@ -313,7 +321,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { } } - 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; @@ -334,7 +342,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider { } const hrefRange = new vscode.Range(linkStart, linkEnd); - if (isLinkInsideCode(codeInDocument, hrefRange)) { + if (noLinkRanges.contains(hrefRange)) { continue; } @@ -355,11 +363,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]; @@ -383,7 +391,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); diff --git a/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts b/extensions/markdown-language-features/src/languageFeatures/dropIntoEditor.ts index c3fb1f55631..d5d7c4d9d29 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) { diff --git a/extensions/markdown-language-features/src/test/diagnostic.test.ts b/extensions/markdown-language-features/src/test/diagnostic.test.ts index 5b631f334de..a9f54978b7d 100644 --- a/extensions/markdown-language-features/src/test/diagnostic.test.ts +++ b/extensions/markdown-language-features/src/test/diagnostic.test.ts @@ -16,16 +16,18 @@ 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, + }, noopToken) + ).diagnostics; } function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration()) { @@ -155,7 +157,26 @@ 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); + }); + + test('Should not generate diagnostics for email autolink', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `a c`, + )); + + const diagnostics = await getComputedDiagnostics(doc1, new InMemoryWorkspaceMarkdownDocuments([doc1])); + assert.deepStrictEqual(diagnostics.length, 0); + }); + + test('Should not generate diagnostics for html tag that looks like an autolink', async () => { + const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines( + `a b c`, + `a b c`, + )); + + const diagnostics = await getComputedDiagnostics(doc1, new InMemoryWorkspaceMarkdownDocuments([doc1])); 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/package.json b/extensions/package.json index 3704c9b327c..b6d00d2faaf 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -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/theme-defaults/themes/dark_vs.json b/extensions/theme-defaults/themes/dark_vs.json index 768a6b3038c..8072b0bdd6e 100644 --- a/extensions/theme-defaults/themes/dark_vs.json +++ b/extensions/theme-defaults/themes/dark_vs.json @@ -12,7 +12,7 @@ "activityBarBadge.background": "#007ACC", "sideBarTitle.foreground": "#BBBBBB", "input.placeholderForeground": "#A6A6A6", - "menu.background": "#252526", + "menu.background": "#303031", "menu.foreground": "#CCCCCC", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D", diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 932626b6a12..e5ca635d50c 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -686,7 +686,7 @@ }, "js/ts.implicitProjectConfig.checkJs": { "type": "boolean", - "default": false, + "default": true, "markdownDescription": "%configuration.implicitProjectConfig.checkJs%", "scope": "window" }, diff --git a/extensions/typescript-language-features/src/utils/configuration.ts b/extensions/typescript-language-features/src/utils/configuration.ts index 3085fd074ce..34355b5e3e5 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', false); + ?? configuration.get('javascript.implicitProjectConfig.checkJs', true); } private static readExperimentalDecorators(configuration: vscode.WorkspaceConfiguration): boolean { diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 8792cd4e6fb..8bb9eb8d3b3 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -17,7 +17,6 @@ "notebookDeprecated", "notebookEditor", "notebookEditorDecorationType", - "notebookEditorEdit", "notebookLiveShare", "notebookMessaging", "notebookMime" diff --git a/extensions/xml/xml.language-configuration.json b/extensions/xml/xml.language-configuration.json index d04db08380c..4706ceec5fb 100644 --- a/extensions/xml/xml.language-configuration.json +++ b/extensions/xml/xml.language-configuration.json @@ -34,7 +34,6 @@ } }, "wordPattern": { - "pattern": "[:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}][-:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}.0-9\\u{B7}\\u{0300}-\\u{036F}\\u{203F}-\\u{2040}]*", - "flags": "u" + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)" } } diff --git a/extensions/xml/xsl.language-configuration.json b/extensions/xml/xsl.language-configuration.json index 5abe96006b5..63c5c75d5c7 100644 --- a/extensions/xml/xsl.language-configuration.json +++ b/extensions/xml/xsl.language-configuration.json @@ -11,8 +11,7 @@ ["[", "]"] ], "wordPattern": { - "pattern": "[:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}][-:A-Z_a-z\\u{C0}-\\u{D6}\\u{D8}-\\u{F6}\\u{F8}-\\u{2FF}\\u{370}-\\u{37D}\\u{37F}-\\u{1FFF}\\u{200C}-\\u{200D}\\u{2070}-\\u{218F}\\u{2C00}-\\u{2FEF}\\u{3001}-\\u{D7FF}\\u{F900}-\\u{FDCF}\\u{FDF0}-\\u{FFFD}\\u{10000}-\\u{EFFFF}.0-9\\u{B7}\\u{0300}-\\u{036F}\\u{203F}-\\u{2040}]*", - "flags": "u" + "pattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\$\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)" } // enhancedBrackets: [{ diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 97766b7943b..1c746b8d2d0 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" @@ -47,10 +47,10 @@ typescript@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== -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 412567abe45..9a63b8672fe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.68.0", - "distro": "ab6870a18e848ce53c0a4b6303132c8fe7d129f2", + "distro": "dec8abea6c1191c66ab4a551172264be30a76e26", "author": { "name": "Microsoft Corporation" }, @@ -85,12 +85,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.29", - "xterm-addon-search": "0.9.0-beta.26", + "xterm": "4.19.0-beta.43", + "xterm-addon-search": "0.9.0-beta.35", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.29", - "xterm-headless": "4.19.0-beta.29", + "xterm-addon-webgl": "0.12.0-beta.34", + "xterm-headless": "4.19.0-beta.43", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -230,4 +230,4 @@ "elliptic": "^6.5.3", "nwmatcher": "^1.4.4" } -} +} \ No newline at end of file diff --git a/remote/package.json b/remote/package.json index ad16af4878c..80e835671ce 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,12 +24,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.29", - "xterm-addon-search": "0.9.0-beta.26", + "xterm": "4.19.0-beta.43", + "xterm-addon-search": "0.9.0-beta.35", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.29", - "xterm-headless": "4.19.0-beta.29", + "xterm-addon-webgl": "0.12.0-beta.34", + "xterm-headless": "4.19.0-beta.43", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index b56da999d19..cdbbf667adf 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -10,9 +10,9 @@ "tas-client-umd": "0.1.5", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.29", - "xterm-addon-search": "0.9.0-beta.26", + "xterm": "4.19.0-beta.43", + "xterm-addon-search": "0.9.0-beta.35", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.29" + "xterm-addon-webgl": "0.12.0-beta.34" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 0e4285693c1..8316dc85a0e 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -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.26: - version "0.9.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.26.tgz#24259b892ce5cd8eff207e4e334dc06776356fe5" - integrity sha512-gOz6v9do7yBDP8e4zdpnDIi3DsyPdLA10lsJDucfMN4nJFM2PjJAsu1fbqq1pXdcu14fHIYzbsp9wIMiW524zQ== +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-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.29: - version "0.12.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" - integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== +xterm-addon-webgl@0.12.0-beta.34: + version "0.12.0-beta.34" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.34.tgz#51cac31cc7a78377be5d481b624ee82948360de1" + integrity sha512-TTIwun+a45oDN54sHhdUxsEx6VflgF2p9YGqS5+gVzpvPrEqP6GoDr6XFCDsZcSqi0ZT2FNGAKWlh7XSxsKQQw== -xterm@4.19.0-beta.29: - version "4.19.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.29.tgz#f0727ddbfe54f3c34a58e57ecbfcbb4d03a30386" - integrity sha512-ZlgrxgotcCB06W0Pk5ClHDkIDE62s1LebgehEsmaksJJtoOQJIxCVu1Kop4EnnPQzZxFaG7uYumfwe0tfd6uWA== +xterm@4.19.0-beta.43: + version "4.19.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.43.tgz#f2113c8ce303d22c5cfad1a4a119b81c103285fa" + integrity sha512-eQ3fzkUApGdl4/rrhzK4OIdMb3+qO0c2iCZIMbeP9SqqDltZnhWncz+3lGa0tnxKizVoUV9kmGaP7orsQ/IavQ== diff --git a/remote/yarn.lock b/remote/yarn.lock index 46f213014ee..a6201c6d433 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -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.26: - version "0.9.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.26.tgz#24259b892ce5cd8eff207e4e334dc06776356fe5" - integrity sha512-gOz6v9do7yBDP8e4zdpnDIi3DsyPdLA10lsJDucfMN4nJFM2PjJAsu1fbqq1pXdcu14fHIYzbsp9wIMiW524zQ== +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-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.29: - version "0.12.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" - integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== +xterm-addon-webgl@0.12.0-beta.34: + version "0.12.0-beta.34" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.34.tgz#51cac31cc7a78377be5d481b624ee82948360de1" + integrity sha512-TTIwun+a45oDN54sHhdUxsEx6VflgF2p9YGqS5+gVzpvPrEqP6GoDr6XFCDsZcSqi0ZT2FNGAKWlh7XSxsKQQw== -xterm-headless@4.19.0-beta.29: - version "4.19.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.29.tgz#9151a1506ddcad3402ce456bbbc6af0828952742" - integrity sha512-wAPyWOp2whY9kT9NL7PMQtvR/A9UO1A4bhP0nOOhZxg9GDeCy5EvsuDn2x+dtsh4jK/L2SZxM6SPHLpNoZpbTQ== +xterm-headless@4.19.0-beta.43: + version "4.19.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.43.tgz#12fe4abe624265240a7de8a922bfc4fd28c5f92a" + integrity sha512-4T8TlWy5u+sS23aPtd8gBHJ0BVljbNQRPMFHzLigDNOMCwc4uWa9JsxYmKteKifcG5aMm11ALPUTxWZCgpATww== -xterm@4.19.0-beta.29: - version "4.19.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.29.tgz#f0727ddbfe54f3c34a58e57ecbfcbb4d03a30386" - integrity sha512-ZlgrxgotcCB06W0Pk5ClHDkIDE62s1LebgehEsmaksJJtoOQJIxCVu1Kop4EnnPQzZxFaG7uYumfwe0tfd6uWA== +xterm@4.19.0-beta.43: + version "4.19.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.43.tgz#f2113c8ce303d22c5cfad1a4a119b81c103285fa" + integrity sha512-eQ3fzkUApGdl4/rrhzK4OIdMb3+qO0c2iCZIMbeP9SqqDltZnhWncz+3lGa0tnxKizVoUV9kmGaP7orsQ/IavQ== yallist@^4.0.0: version "4.0.0" 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/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/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index 42bbf152ec5..61340ea1ba2 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -9,6 +9,8 @@ import { $, addDisposableListener, append, EventHelper, EventLike, EventType } f import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { ISelectBoxOptions, ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { Action, ActionRunner, IAction, IActionChangeEvent, IActionRunner, Separator } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -28,7 +30,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { element: HTMLElement | undefined; _context: unknown; - _action: IAction; + readonly _action: IAction; get action() { return this._action; @@ -234,6 +236,7 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions { icon?: boolean; label?: boolean; keybinding?: string | null; + hoverDelegate?: IHoverDelegate; } export class ActionViewItem extends BaseActionViewItem { @@ -242,6 +245,7 @@ export class ActionViewItem extends BaseActionViewItem { protected override options: IActionViewItemOptions; private cssClass?: string; + private customHover?: ICustomHover; constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { super(context, action, options); @@ -326,10 +330,23 @@ export class ActionViewItem extends BaseActionViewItem { title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); } } + this._applyUpdateTooltip(title); + } + protected _applyUpdateTooltip(title: string | undefined | null): void { if (title && this.label) { - this.label.title = title; this.label.setAttribute('aria-label', title); + if (!this.options.hoverDelegate) { + this.label.title = title; + } else { + this.label.title = ''; + if (!this.customHover) { + this.customHover = setupCustomHover(this.options.hoverDelegate, this.label, title); + this._store.add(this.customHover); + } else { + this.customHover.update(title); + } + } } } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 2a5548664c0..7280c8a4737 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -6,6 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { ActionRunner, IAction, IActionRunner, IRunEvent, Separator } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -49,6 +50,7 @@ export interface IActionBarOptions { readonly allowContextMenu?: boolean; readonly preventLoopNavigation?: boolean; readonly focusOnlyEnabledItems?: boolean; + readonly hoverDelegate?: IHoverDelegate; } export interface IActionOptions extends IActionViewItemOptions { @@ -327,7 +329,7 @@ export class ActionBar extends Disposable implements IActionRunner { } if (!item) { - item = new ActionViewItem(this.context, action, options); + item = new ActionViewItem(this.context, action, { hoverDelegate: this.options.hoverDelegate, ...options }); } // Prevent native context menu on actions 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/editor/common/dnd.ts b/src/vs/base/common/dataTransfer.ts similarity index 100% rename from src/vs/editor/common/dnd.ts rename to src/vs/base/common/dataTransfer.ts 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/editor/browser/config/editorConfiguration.ts b/src/vs/editor/browser/config/editorConfiguration.ts index 58474ac9363..e3f41e0d416 100644 --- a/src/vs/editor/browser/config/editorConfiguration.ts +++ b/src/vs/editor/browser/config/editorConfiguration.ts @@ -30,12 +30,6 @@ export interface IEditorConstructionOptions extends IEditorOptions { * Defaults to an internal DOM node. */ overflowWidgetsDomNode?: HTMLElement; - /** - * Enables dropping into the editor. - * - * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. - */ - enableDropIntoEditor?: boolean; } export class EditorConfiguration extends Disposable implements IEditorConfiguration { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 2dcbf601e37..991afab9ef8 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -14,7 +14,7 @@ import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, EmitterOptions, Event, EventDeliveryQueue } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { EditorConfiguration, IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration'; import * as editorBrowser from 'vs/editor/browser/editorBrowser'; @@ -368,35 +368,42 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._actions[internalAction.id] = internalAction; }); - if (_options.enableDropIntoEditor) { - this._register(new dom.DragAndDropObserver(this._domElement, { - onDragEnter: () => undefined, - onDragOver: e => { - const target = this.getTargetAtClientPoint(e.clientX, e.clientY); - if (target?.position) { - this.showDropIndicatorAt(target.position); - } - }, - onDrop: async e => { - this.removeDropIndicator(); + this._register(new dom.DragAndDropObserver(this._domElement, { + onDragEnter: () => undefined, + onDragOver: e => { + if (!this._configuration.options.get(EditorOption.enableDropIntoEditor)) { + return; + } - if (!e.dataTransfer) { - return; - } + const target = this.getTargetAtClientPoint(e.clientX, e.clientY); + if (target?.position) { + this.showDropIndicatorAt(target.position); + } + }, + onDrop: async e => { + if (!this._configuration.options.get(EditorOption.enableDropIntoEditor)) { + return; + } + + this.removeDropIndicator(); + + if (!e.dataTransfer) { + return; + } + + const target = this.getTargetAtClientPoint(e.clientX, e.clientY); + if (target?.position) { + this._onDropIntoEditor.fire({ position: target.position, event: e }); + } + }, + onDragLeave: () => { + this.removeDropIndicator(); + }, + onDragEnd: () => { + this.removeDropIndicator(); + }, + })); - const target = this.getTargetAtClientPoint(e.clientX, e.clientY); - if (target?.position) { - this._onDropIntoEditor.fire({ position: target.position, event: e }); - } - }, - onDragLeave: () => { - this.removeDropIndicator(); - }, - onDragEnd: () => { - this.removeDropIndicator(); - }, - })); - } this._codeEditorService.addCodeEditor(this); } @@ -1255,6 +1262,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData.viewModel.executeCommands(commands, source); } + public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): EditorDecorationsCollection { + return new EditorDecorationsCollection(this, decorations); + } + public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { if (!this._modelData) { // callback will not be called @@ -2131,6 +2142,82 @@ class CodeEditorWidgetFocusTracker extends Disposable { } } +class EditorDecorationsCollection implements editorCommon.IEditorDecorationsCollection { + + private _decorationIds: string[] = []; + private _isChangingDecorations: boolean = false; + + public get length(): number { + return this._decorationIds.length; + } + + constructor( + private readonly _editor: editorBrowser.ICodeEditor, + decorations: IModelDeltaDecoration[] | undefined + ) { + if (Array.isArray(decorations) && decorations.length > 0) { + this.set(decorations); + } + } + + public onDidChange(listener: (e: IModelDecorationsChangedEvent) => any, thisArgs?: any, disposables?: IDisposable[] | DisposableStore): IDisposable { + return this._editor.onDidChangeModelDecorations((e) => { + if (this._isChangingDecorations) { + return; + } + listener.call(thisArgs, e); + }, disposables); + } + + public getRange(index: number): Range | null { + if (!this._editor.hasModel()) { + return null; + } + if (index >= this._decorationIds.length) { + return null; + } + return this._editor.getModel().getDecorationRange(this._decorationIds[index]); + } + + public getRanges(): Range[] { + if (!this._editor.hasModel()) { + return []; + } + const model = this._editor.getModel(); + const result: Range[] = []; + for (const decorationId of this._decorationIds) { + const range = model.getDecorationRange(decorationId); + if (range) { + result.push(range); + } + } + return result; + } + + public has(decoration: IModelDecoration): boolean { + return this._decorationIds.includes(decoration.id); + } + + public clear(): void { + if (this._decorationIds.length === 0) { + // nothing to do + return; + } + this.set([]); + } + + public set(newDecorations: IModelDeltaDecoration[]): void { + try { + this._isChangingDecorations = true; + this._editor.changeDecorations((accessor) => { + this._decorationIds = accessor.deltaDecorations(this._decorationIds, newDecorations); + }); + } finally { + this._isChangingDecorations = false; + } + } +} + const squigglyStart = encodeURIComponent(``); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index a29e3b980fc..e9e666a6ce0 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -920,6 +920,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._modifiedEditor.trigger(source, handlerId, payload); } + public createDecorationsCollection(decorations?: IModelDeltaDecoration[]): editorCommon.IEditorDecorationsCollection { + return this._modifiedEditor.createDecorationsCollection(decorations); + } + public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { return this._modifiedEditor.changeDecorations(callback); } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index feb0bd01946..022895e3e55 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -659,6 +659,13 @@ export interface IEditorOptions { * Configures bracket pair colorization (disabled by default). */ bracketPairColorization?: IBracketPairColorizationOptions; + + /** + * Enables dropping into the editor from an external source. + * + * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. + */ + enableDropIntoEditor?: boolean; } /** @@ -4431,6 +4438,7 @@ export const enum EditorOption { disableMonospaceOptimizations, domReadOnly, dragAndDrop, + enableDropIntoEditor, emptySelectionClipboard, extraEditorClassName, fastScrollSensitivity, @@ -4739,6 +4747,9 @@ export const EditorOptions = { { description: nls.localize('dragAndDrop', "Controls whether the editor should allow moving selections via drag and drop.") } )), emptySelectionClipboard: register(new EditorEmptySelectionClipboard()), + enableDropIntoEditor: register(new EditorBooleanOption( + EditorOption.enableDropIntoEditor, 'enableDropIntoEditor', true + )), extraEditorClassName: register(new EditorStringOption( EditorOption.extraEditorClassName, 'extraEditorClassName', '', )), diff --git a/src/vs/editor/common/core/wordHelper.ts b/src/vs/editor/common/core/wordHelper.ts index 9f98fe1a524..a4051dc26e8 100644 --- a/src/vs/editor/common/core/wordHelper.ts +++ b/src/vs/editor/common/core/wordHelper.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Iterable } from 'vs/base/common/iterator'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { LinkedList } from 'vs/base/common/linkedList'; + export const USUAL_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?'; /** @@ -71,13 +75,31 @@ export function ensureValidWordDefinition(wordDefinition?: RegExp | null): RegEx return result; } -const _defaultConfig = { + +export interface IGetWordAtTextConfig { + maxLen: number; + windowSize: number; + timeBudget: number; +} + + +const _defaultConfig = new LinkedList(); +_defaultConfig.unshift({ maxLen: 1000, windowSize: 15, timeBudget: 150 -}; +}); -export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number, config = _defaultConfig): IWordAtPosition | null { +export function setDefaultGetWordAtTextConfig(value: IGetWordAtTextConfig) { + const rm = _defaultConfig.unshift(value); + return toDisposable(rm); +} + +export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number, config?: IGetWordAtTextConfig): IWordAtPosition | null { + + if (!config) { + config = Iterable.first(_defaultConfig)!; + } if (text.length > config.maxLen) { // don't throw strings that long at the regexp diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 69bcb525d4d..9260b986d88 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -11,9 +11,10 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness, IValidEditOperation } from 'vs/editor/common/model'; +import { IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness, IValidEditOperation, IModelDeltaDecoration, IModelDecoration } from 'vs/editor/common/model'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { IDimension } from 'vs/editor/common/core/dimension'; +import { IModelDecorationsChangedEvent } from 'vs/editor/common/textModelEvents'; /** * A builder and helper for edit operations for a command. @@ -459,6 +460,13 @@ export interface IEditor { */ setModel(model: IEditorModel | null): void; + /** + * Create a collection of decorations. All decorations added through this collection + * will get the ownerId of the editor (meaning they will not show up in other editors). + * These decorations will be automatically cleared when the editor's model changes. + */ + createDecorationsCollection(decorations?: IModelDeltaDecoration[]): IEditorDecorationsCollection; + /** * Change the decorations. All decorations added through this changeAccessor * will get the ownerId of the editor (meaning they will not show up in other @@ -509,6 +517,40 @@ export interface ICompositeCodeEditor { // readonly editors: readonly ICodeEditor[] maybe supported with uris } +/** + * A collection of decorations + */ +export interface IEditorDecorationsCollection { + /** + * An event emitted when decorations change in the editor, + * but the change is not caused by us setting or clearing the collection. + */ + onDidChange: Event; + /** + * Get the decorations count. + */ + length: number; + /** + * Get the range for a decoration. + */ + getRange(index: number): Range | null; + /** + * Get all ranges for decorations. + */ + getRanges(): Range[]; + /** + * Determine if a decoration is in this collection. + */ + has(decoration: IModelDecoration): boolean; + /** + * Replace all previous decorations with `newDecorations`. + */ + set(newDecorations: IModelDeltaDecoration[]): void; + /** + * Remove all previous decorations. + */ + clear(): void; +} /** * An editor contribution that gets created every time a new editor gets created and gets disposed when the editor gets disposed. diff --git a/src/vs/editor/common/languageFeatureRegistry.ts b/src/vs/editor/common/languageFeatureRegistry.ts index 74995dbe597..aa686680ac9 100644 --- a/src/vs/editor/common/languageFeatureRegistry.ts +++ b/src/vs/editor/common/languageFeatureRegistry.ts @@ -39,13 +39,15 @@ class MatchCandidate { constructor( readonly uri: URI, readonly languageId: string, + readonly notebookUri: URI | undefined, readonly notebookType: string | undefined ) { } equals(other: MatchCandidate): boolean { return this.notebookType === other.notebookType && this.languageId === other.languageId - && this.uri.toString() === other.uri.toString(); + && this.uri.toString() === other.uri.toString() + && this.notebookUri?.toString() === other.notebookUri?.toString(); } } @@ -151,8 +153,8 @@ export class LanguageFeatureRegistry { // use the uri (scheme, pattern) of the notebook info iff we have one // otherwise it's the model's/document's uri const candidate = notebookInfo - ? new MatchCandidate(notebookInfo.uri, model.getLanguageId(), notebookInfo.type) - : new MatchCandidate(model.uri, model.getLanguageId(), undefined); + ? new MatchCandidate(model.uri, model.getLanguageId(), notebookInfo.uri, notebookInfo.type) + : new MatchCandidate(model.uri, model.getLanguageId(), undefined, undefined); if (this._lastCandidate?.equals(candidate)) { // nothing has changed @@ -162,7 +164,7 @@ export class LanguageFeatureRegistry { this._lastCandidate = candidate; for (let entry of this._entries) { - entry._score = score(entry.selector, candidate.uri, candidate.languageId, shouldSynchronizeModel(model), candidate.notebookType); + entry._score = score(entry.selector, candidate.uri, candidate.languageId, shouldSynchronizeModel(model), candidate.notebookUri, candidate.notebookType); if (isExclusive(entry.selector) && entry._score > 0) { // support for one exclusive selector that overwrites diff --git a/src/vs/editor/common/languageSelector.ts b/src/vs/editor/common/languageSelector.ts index 64a8440059f..b8b245d067f 100644 --- a/src/vs/editor/common/languageSelector.ts +++ b/src/vs/editor/common/languageSelector.ts @@ -21,13 +21,13 @@ export interface LanguageFilter { export type LanguageSelector = string | LanguageFilter | ReadonlyArray; -export function score(selector: LanguageSelector | undefined, candidateUri: URI, candidateLanguage: string, candidateIsSynchronized: boolean, candidateNotebookType: string | undefined): number { +export function score(selector: LanguageSelector | undefined, candidateUri: URI, candidateLanguage: string, candidateIsSynchronized: boolean, candidateNotebookUri: URI | undefined, candidateNotebookType: string | undefined): number { if (Array.isArray(selector)) { // array -> take max individual value let ret = 0; for (const filter of selector) { - const value = score(filter, candidateUri, candidateLanguage, candidateIsSynchronized, candidateNotebookType); + const value = score(filter, candidateUri, candidateLanguage, candidateIsSynchronized, candidateNotebookUri, candidateNotebookType); if (value === 10) { return value; // already at the highest } @@ -62,6 +62,12 @@ export function score(selector: LanguageSelector | undefined, candidateUri: URI, return 0; } + // selector targets a notebook -> use the notebook uri instead + // of the "normal" document uri. + if (notebookType && candidateNotebookUri) { + candidateUri = candidateNotebookUri; + } + let ret = 0; if (scheme) { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 74306ee05d9..b5c7361ce88 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -4,11 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -16,10 +19,7 @@ import * as model from 'vs/editor/common/model'; import { TokenizationRegistry as TokenizationRegistryImpl } from 'vs/editor/common/tokenizationRegistry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IMarkerData } from 'vs/platform/markers/common/markers'; -import { Codicon, CSSIcon } from 'vs/base/common/codicons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; -import { IDataTransfer } from 'vs/editor/common/dnd'; /** * Open ended enum at runtime diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index cdcd12ecfa2..9134e45ba68 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -710,6 +710,14 @@ class SemanticColoringFeature extends Disposable { })); this._register(themeService.onDidColorThemeChange(handleSettingOrThemeChange)); } + + override dispose(): void { + // Dispose all watchers + for (const watcher of Object.values(this._watchers)) { + watcher.dispose(); + } + super.dispose(); + } } class SemanticStyling extends Disposable { @@ -900,6 +908,8 @@ export class ModelSemanticColoring extends Disposable { } private static _copy(src: Uint32Array, srcOffset: number, dest: Uint32Array, destOffset: number, length: number): void { + // protect against overflows + length = Math.min(length, dest.length - destOffset, src.length - srcOffset); for (let i = 0; i < length; i++) { dest[destOffset + i] = src[srcOffset + i]; } @@ -960,6 +970,13 @@ export class ModelSemanticColoring extends Disposable { for (let i = tokens.edits.length - 1; i >= 0; i--) { const edit = tokens.edits[i]; + if (edit.start > srcData.length) { + styling.warnInvalidEditStart(currentResponse.resultId, tokens.resultId, i, edit.start, srcData.length); + // The edits are invalid and there's no way to recover + this._model.tokenization.setSemanticTokens(null, true); + return; + } + const copyCount = srcLastStart - (edit.start + edit.deleteCount); if (copyCount > 0) { ModelSemanticColoring._copy(srcData, srcLastStart - copyCount, destData, destLastStart - copyCount, copyCount); diff --git a/src/vs/editor/common/services/semanticTokensProviderStyling.ts b/src/vs/editor/common/services/semanticTokensProviderStyling.ts index 6aa0cd24b60..e804a241d4e 100644 --- a/src/vs/editor/common/services/semanticTokensProviderStyling.ts +++ b/src/vs/editor/common/services/semanticTokensProviderStyling.ts @@ -16,8 +16,9 @@ export const enum SemanticTokensProviderStylingConstants { export class SemanticTokensProviderStyling { private readonly _hashTable: HashTable; - private _hasWarnedOverlappingTokens: boolean; - private _hasWarnedInvalidLengthTokens: boolean; + private _hasWarnedOverlappingTokens = false; + private _hasWarnedInvalidLengthTokens = false; + private _hasWarnedInvalidEditStart = false; constructor( private readonly _legend: SemanticTokensLegend, @@ -26,8 +27,6 @@ export class SemanticTokensProviderStyling { @ILogService private readonly _logService: ILogService ) { this._hashTable = new HashTable(); - this._hasWarnedOverlappingTokens = false; - this._hasWarnedInvalidLengthTokens = false; } public getMetadata(tokenTypeIndex: number, tokenModifierSet: number, languageId: string): number { @@ -116,6 +115,13 @@ export class SemanticTokensProviderStyling { } } + public warnInvalidEditStart(previousResultId: string | undefined, resultId: string | undefined, editIndex: number, editStart: number, maxExpectedStart: number): void { + if (!this._hasWarnedInvalidEditStart) { + this._hasWarnedInvalidEditStart = true; + console.warn(`Invalid semantic tokens edit detected (previousResultId: ${previousResultId}, resultId: ${resultId}) at edit #${editIndex}: The provided start offset ${editStart} is outside the previous data (length ${maxExpectedStart}).`); + } + } + } const enum SemanticColoringConstants { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 265cb543054..8d709286d5c 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -199,107 +199,108 @@ export enum EditorOption { disableMonospaceOptimizations = 29, domReadOnly = 30, dragAndDrop = 31, - emptySelectionClipboard = 32, - extraEditorClassName = 33, - fastScrollSensitivity = 34, - find = 35, - fixedOverflowWidgets = 36, - folding = 37, - foldingStrategy = 38, - foldingHighlight = 39, - foldingImportsByDefault = 40, - foldingMaximumRegions = 41, - unfoldOnClickAfterEndOfLine = 42, - fontFamily = 43, - fontInfo = 44, - fontLigatures = 45, - fontSize = 46, - fontWeight = 47, - formatOnPaste = 48, - formatOnType = 49, - glyphMargin = 50, - gotoLocation = 51, - hideCursorInOverviewRuler = 52, - hover = 53, - inDiffEditor = 54, - inlineSuggest = 55, - letterSpacing = 56, - lightbulb = 57, - lineDecorationsWidth = 58, - lineHeight = 59, - lineNumbers = 60, - lineNumbersMinChars = 61, - linkedEditing = 62, - links = 63, - matchBrackets = 64, - minimap = 65, - mouseStyle = 66, - mouseWheelScrollSensitivity = 67, - mouseWheelZoom = 68, - multiCursorMergeOverlapping = 69, - multiCursorModifier = 70, - multiCursorPaste = 71, - occurrencesHighlight = 72, - overviewRulerBorder = 73, - overviewRulerLanes = 74, - padding = 75, - parameterHints = 76, - peekWidgetDefaultFocus = 77, - definitionLinkOpensInPeek = 78, - quickSuggestions = 79, - quickSuggestionsDelay = 80, - readOnly = 81, - renameOnType = 82, - renderControlCharacters = 83, - renderFinalNewline = 84, - renderLineHighlight = 85, - renderLineHighlightOnlyWhenFocus = 86, - renderValidationDecorations = 87, - renderWhitespace = 88, - revealHorizontalRightPadding = 89, - roundedSelection = 90, - rulers = 91, - scrollbar = 92, - scrollBeyondLastColumn = 93, - scrollBeyondLastLine = 94, - scrollPredominantAxis = 95, - selectionClipboard = 96, - selectionHighlight = 97, - selectOnLineNumbers = 98, - showFoldingControls = 99, - showUnused = 100, - snippetSuggestions = 101, - smartSelect = 102, - smoothScrolling = 103, - stickyTabStops = 104, - stopRenderingLineAfter = 105, - suggest = 106, - suggestFontSize = 107, - suggestLineHeight = 108, - suggestOnTriggerCharacters = 109, - suggestSelection = 110, - tabCompletion = 111, - tabIndex = 112, - unicodeHighlighting = 113, - unusualLineTerminators = 114, - useShadowDOM = 115, - useTabStops = 116, - wordSeparators = 117, - wordWrap = 118, - wordWrapBreakAfterCharacters = 119, - wordWrapBreakBeforeCharacters = 120, - wordWrapColumn = 121, - wordWrapOverride1 = 122, - wordWrapOverride2 = 123, - wrappingIndent = 124, - wrappingStrategy = 125, - showDeprecated = 126, - inlayHints = 127, - editorClassName = 128, - pixelRatio = 129, - tabFocusMode = 130, - layoutInfo = 131, - wrappingInfo = 132 + enableDropIntoEditor = 32, + emptySelectionClipboard = 33, + extraEditorClassName = 34, + fastScrollSensitivity = 35, + find = 36, + fixedOverflowWidgets = 37, + folding = 38, + foldingStrategy = 39, + foldingHighlight = 40, + foldingImportsByDefault = 41, + foldingMaximumRegions = 42, + unfoldOnClickAfterEndOfLine = 43, + fontFamily = 44, + fontInfo = 45, + fontLigatures = 46, + fontSize = 47, + fontWeight = 48, + formatOnPaste = 49, + formatOnType = 50, + glyphMargin = 51, + gotoLocation = 52, + hideCursorInOverviewRuler = 53, + hover = 54, + inDiffEditor = 55, + inlineSuggest = 56, + letterSpacing = 57, + lightbulb = 58, + lineDecorationsWidth = 59, + lineHeight = 60, + lineNumbers = 61, + lineNumbersMinChars = 62, + linkedEditing = 63, + links = 64, + matchBrackets = 65, + minimap = 66, + mouseStyle = 67, + mouseWheelScrollSensitivity = 68, + mouseWheelZoom = 69, + multiCursorMergeOverlapping = 70, + multiCursorModifier = 71, + multiCursorPaste = 72, + occurrencesHighlight = 73, + overviewRulerBorder = 74, + overviewRulerLanes = 75, + padding = 76, + parameterHints = 77, + peekWidgetDefaultFocus = 78, + definitionLinkOpensInPeek = 79, + quickSuggestions = 80, + quickSuggestionsDelay = 81, + readOnly = 82, + renameOnType = 83, + renderControlCharacters = 84, + renderFinalNewline = 85, + renderLineHighlight = 86, + renderLineHighlightOnlyWhenFocus = 87, + renderValidationDecorations = 88, + renderWhitespace = 89, + revealHorizontalRightPadding = 90, + roundedSelection = 91, + rulers = 92, + scrollbar = 93, + scrollBeyondLastColumn = 94, + scrollBeyondLastLine = 95, + scrollPredominantAxis = 96, + selectionClipboard = 97, + selectionHighlight = 98, + selectOnLineNumbers = 99, + showFoldingControls = 100, + showUnused = 101, + snippetSuggestions = 102, + smartSelect = 103, + smoothScrolling = 104, + stickyTabStops = 105, + stopRenderingLineAfter = 106, + suggest = 107, + suggestFontSize = 108, + suggestLineHeight = 109, + suggestOnTriggerCharacters = 110, + suggestSelection = 111, + tabCompletion = 112, + tabIndex = 113, + unicodeHighlighting = 114, + unusualLineTerminators = 115, + useShadowDOM = 116, + useTabStops = 117, + wordSeparators = 118, + wordWrap = 119, + wordWrapBreakAfterCharacters = 120, + wordWrapBreakBeforeCharacters = 121, + wordWrapColumn = 122, + wordWrapOverride1 = 123, + wordWrapOverride2 = 124, + wrappingIndent = 125, + wrappingStrategy = 126, + showDeprecated = 127, + inlayHints = 128, + editorClassName = 129, + pixelRatio = 130, + tabFocusMode = 131, + layoutInfo = 132, + wrappingInfo = 133 } /** diff --git a/src/vs/editor/contrib/anchorSelect/browser/anchorSelect.ts b/src/vs/editor/contrib/anchorSelect/browser/anchorSelect.ts index ceaefc19bb1..6972a300abb 100644 --- a/src/vs/editor/contrib/anchorSelect/browser/anchorSelect.ts +++ b/src/vs/editor/contrib/anchorSelect/browser/anchorSelect.ts @@ -43,17 +43,20 @@ class SelectionAnchorController implements IEditorContribution { setSelectionAnchor(): void { if (this.editor.hasModel()) { const position = this.editor.getPosition(); - const previousDecorations = this.decorationId ? [this.decorationId] : []; - const newDecorationId = this.editor.deltaDecorations(previousDecorations, [{ - range: Selection.fromPositions(position, position), - options: { - description: 'selection-anchor', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - hoverMessage: new MarkdownString().appendText(localize('selectionAnchor', "Selection Anchor")), - className: 'selection-anchor' + this.editor.changeDecorations((accessor) => { + if (this.decorationId) { + accessor.removeDecoration(this.decorationId); } - }]); - this.decorationId = newDecorationId[0]; + this.decorationId = accessor.addDecoration( + Selection.fromPositions(position, position), + { + description: 'selection-anchor', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + hoverMessage: new MarkdownString().appendText(localize('selectionAnchor', "Selection Anchor")), + className: 'selection-anchor' + } + ); + }); this.selectionAnchorSetContextKey.set(!!this.decorationId); alert(localize('anchorSet', "Anchor set at {0}:{1}", position.lineNumber, position.column)); } @@ -81,8 +84,11 @@ class SelectionAnchorController implements IEditorContribution { cancelSelectionAnchor(): void { if (this.decorationId) { - this.editor.deltaDecorations([this.decorationId], []); - this.decorationId = undefined; + const decorationId = this.decorationId; + this.editor.changeDecorations((accessor) => { + accessor.removeDecoration(decorationId); + this.decorationId = undefined; + }); this.selectionAnchorSetContextKey.set(false); } } diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts index 37b38180c3a..0f917f1b5c6 100644 --- a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.ts @@ -13,7 +13,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -105,7 +105,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont private _lastBracketsData: BracketsData[]; private _lastVersionId: number; - private _decorations: string[]; + private readonly _decorations: IEditorDecorationsCollection; private readonly _updateBracketsSoon: RunOnceScheduler; private _matchBrackets: 'never' | 'near' | 'always'; @@ -116,7 +116,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont this._editor = editor; this._lastBracketsData = []; this._lastVersionId = 0; - this._decorations = []; + this._decorations = this._editor.createDecorationsCollection(); this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50)); this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets); @@ -136,7 +136,6 @@ export class BracketMatchingController extends Disposable implements IEditorCont })); this._register(editor.onDidChangeModel((e) => { this._lastBracketsData = []; - this._decorations = []; this._updateBracketsSoon.schedule(); })); this._register(editor.onDidChangeModelLanguageConfiguration((e) => { @@ -146,7 +145,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont this._register(editor.onDidChangeConfiguration((e) => { if (e.hasChanged(EditorOption.matchBrackets)) { this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets); - this._decorations = this._editor.deltaDecorations(this._decorations, []); + this._decorations.clear(); this._lastBracketsData = []; this._lastVersionId = 0; this._updateBracketsSoon.schedule(); @@ -285,7 +284,7 @@ export class BracketMatchingController extends Disposable implements IEditorCont } } - this._decorations = this._editor.deltaDecorations(this._decorations, newDecorations); + this._decorations.set(newDecorations); } private _recomputeBrackets(): void { diff --git a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts index 2a312a7c9b8..cf9a2bfcd47 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorDetector.ts @@ -16,7 +16,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IModelDecoration, IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; @@ -41,7 +41,7 @@ export class ColorDetector extends Disposable implements IEditorContribution { private _decorationsIds: string[] = []; private _colorDatas = new Map(); - private _colorDecoratorIds: ReadonlySet = new Set(); + private readonly _colorDecoratorIds = this._editor.createDecorationsCollection(); private _isEnabled: boolean; @@ -215,12 +215,12 @@ export class ColorDetector extends Disposable implements IEditorContribution { }); } - this._colorDecoratorIds = new Set(this._editor.deltaDecorations([...this._colorDecoratorIds], decorations)); + this._colorDecoratorIds.set(decorations); } private removeAllDecorations(): void { this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, []); - this._colorDecoratorIds = new Set(this._editor.deltaDecorations([...this._colorDecoratorIds], [])); + this._colorDecoratorIds.clear(); this._colorDecorationClassRefs.clear(); } @@ -241,8 +241,8 @@ export class ColorDetector extends Disposable implements IEditorContribution { return this._colorDatas.get(decorations[0].id)!; } - isColorDecorationId(decorationId: string): boolean { - return this._colorDecoratorIds.has(decorationId); + isColorDecoration(decoration: IModelDecoration): boolean { + return this._colorDecoratorIds.has(decoration); } } diff --git a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts index 406407f5fd4..adb039eecf6 100644 --- a/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts +++ b/src/vs/editor/contrib/colorPicker/browser/colorHoverParticipant.ts @@ -70,7 +70,7 @@ export class ColorHoverParticipant implements IEditorHoverParticipant this._onEditorMouseDown(e))); this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e))); this._register(this._editor.onMouseDrag((e: IEditorMouseEvent) => this._onEditorMouseDrag(e))); @@ -57,7 +57,6 @@ export class DragAndDropController extends Disposable implements IEditorContribu this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e))); this._register(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur())); this._register(this._editor.onDidBlurEditorText(() => this.onEditorBlur())); - this._dndDecorationIds = []; this._mouseDown = false; this._modifierPressed = false; this._dragSelection = null; @@ -209,17 +208,15 @@ export class DragAndDropController extends Disposable implements IEditorContribu }); public showAt(position: Position): void { - let newDecorations: IModelDeltaDecoration[] = [{ + this._dndDecorationIds.set([{ range: new Range(position.lineNumber, position.column, position.lineNumber, position.column), options: DragAndDropController._DECORATION_OPTIONS - }]; - - this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, newDecorations); + }]); this._editor.revealPosition(position, ScrollType.Immediate); } private _removeDecoration(): void { - this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, []); + this._dndDecorationIds.clear(); } private _hitContent(target: IMouseTarget): boolean { diff --git a/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts similarity index 62% rename from src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts rename to src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index def227ff8b3..d113f9e513c 100644 --- a/src/vs/workbench/contrib/dropIntoEditor/browser/dropIntoEditor.contibution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -5,6 +5,7 @@ import { distinct } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; import { Disposable } from 'vs/base/common/lifecycle'; import { Mimes } from 'vs/base/common/mime'; import { relativePath } from 'vs/base/common/resources'; @@ -13,13 +14,15 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IDataTransfer, IDataTransferItem } from 'vs/editor/common/dnd'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { DocumentOnDropEditProvider, SnippetTextEdit } from 'vs/editor/common/languages'; +import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { performSnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { extractEditorsDropData, FileAdditionalNativeProperties } from 'vs/platform/dnd/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; export class DropIntoEditorController extends Disposable implements IEditorContribution { @@ -28,13 +31,31 @@ export class DropIntoEditorController extends Disposable implements IEditorContr constructor( editor: ICodeEditor, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super(); - editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event)); + this._register(editor.onDropIntoEditor(e => this.onDropIntoEditor(editor, e.position, e.event))); + + + this._languageFeaturesService.documentOnDropEditProvider.register('*', new DefaultOnDropProvider(workspaceContextService)); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('workbench.experimental.editor.dropIntoEditor.enabled')) { + this.updateEditorOptions(editor); + } + })); + + this.updateEditorOptions(editor); + } + + private updateEditorOptions(editor: ICodeEditor) { + editor.updateOptions({ + enableDropIntoEditor: this._configurationService.getValue('workbench.experimental.editor.dropIntoEditor.enabled') + }); } private async onDropIntoEditor(editor: ICodeEditor, position: IPosition, dragEvent: DragEvent) { @@ -45,7 +66,39 @@ export class DropIntoEditorController extends Disposable implements IEditorContr const model = editor.getModel(); const modelVersionNow = model.getVersionId(); + const ourDataTransfer = await this.extractDataTransferData(dragEvent); + if (ourDataTransfer.size === 0) { + return; + } + + if (editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + const cts = new CancellationTokenSource(); + editor.onDidDispose(() => cts.cancel()); + model.onDidChangeContent(() => cts.cancel()); + + const providers = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); + for (const provider of providers) { + const edit = await provider.provideDocumentOnDropEdits(model, position, ourDataTransfer, cts.token); + if (cts.token.isCancellationRequested || editor.getModel().getVersionId() !== modelVersionNow) { + return; + } + + if (edit) { + performSnippetEdit(editor, edit); + return; + } + } + } + + public async extractDataTransferData(dragEvent: DragEvent): Promise { const textEditorDataTransfer: IDataTransfer = new Map(); + if (!dragEvent.dataTransfer) { + return textEditorDataTransfer; + } + for (const item of dragEvent.dataTransfer.items) { const type = item.type; if (item.kind === 'string') { @@ -61,7 +114,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr textEditorDataTransfer.set(type, { asString: () => Promise.resolve(''), asFile: () => { - const uri = file.path ? URI.parse(file.path) : undefined; + const uri = (file as FileAdditionalNativeProperties).path ? URI.parse((file as FileAdditionalNativeProperties).path!) : undefined; return { name: file.name, uri: uri, @@ -76,66 +129,52 @@ export class DropIntoEditorController extends Disposable implements IEditorContr } } - if (!textEditorDataTransfer.has(Mimes.uriList.toLowerCase())) { + if (!textEditorDataTransfer.has(Mimes.uriList)) { const editorData = (await this._instantiationService.invokeFunction(extractEditorsDropData, dragEvent)) .filter(input => input.resource) .map(input => input.resource!.toString()); if (editorData.length) { + const added: IDataTransfer = new Map(); + const str = distinct(editorData).join('\n'); - textEditorDataTransfer.set(Mimes.uriList.toLowerCase(), { - asString: () => Promise.resolve(str), + added.set(Mimes.uriList.toLowerCase(), { asFile: () => undefined, - value: undefined + asString: async () => str, + value: str, }); + return added; } } - if (textEditorDataTransfer.size === 0) { - return; - } - - if (editor.getModel().getVersionId() !== modelVersionNow) { - return; - } - - const cts = new CancellationTokenSource(); - editor.onDidDispose(() => cts.cancel()); - model.onDidChangeContent(() => cts.cancel()); - - const ordered = this._languageFeaturesService.documentOnDropEditProvider.ordered(model); - for (const provider of ordered) { - const edit = await provider.provideDocumentOnDropEdits(model, position, textEditorDataTransfer, cts.token); - if (cts.token.isCancellationRequested || editor.getModel().getVersionId() !== modelVersionNow) { - return; - } - - if (edit) { - performSnippetEdit(editor, edit); - return; - } - } - - return this.doDefaultDrop(editor, position, textEditorDataTransfer, cts.token); + return textEditorDataTransfer; } +} - private async doDefaultDrop(editor: ICodeEditor, position: IPosition, textEditorDataTransfer: IDataTransfer, token: CancellationToken): Promise { +class DefaultOnDropProvider implements DocumentOnDropEditProvider { + + constructor( + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + ) { } + + async provideDocumentOnDropEdits(model: ITextModel, position: IPosition, dataTransfer: IDataTransfer, _token: CancellationToken): Promise { const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column); - const urlListEntry = textEditorDataTransfer.get('text/uri-list'); + const urlListEntry = dataTransfer.get('text/uri-list'); if (urlListEntry) { const urlList = await urlListEntry.asString(); - return this.doUriListDrop(editor, range, urlList, token); + return this.doUriListDrop(range, urlList); } - const textEntry = textEditorDataTransfer.get('text') ?? textEditorDataTransfer.get(Mimes.text); + const textEntry = dataTransfer.get('text') ?? dataTransfer.get(Mimes.text); if (textEntry) { const text = await textEntry.asString(); - performSnippetEdit(editor, { range, snippet: text }); + return { range, snippet: text }; } + return undefined; } - private async doUriListDrop(editor: ICodeEditor, range: Range, urlList: string, token: CancellationToken): Promise { + private doUriListDrop(range: Range, urlList: string): SnippetTextEdit | undefined { const uris: URI[] = []; for (const resource of urlList.split('\n')) { try { @@ -162,7 +201,7 @@ export class DropIntoEditorController extends Disposable implements IEditorContr }) .join(' '); - performSnippetEdit(editor, { range, snippet }); + return { range, snippet }; } } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts index 75af03dba4c..cb1b242d7a3 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts @@ -213,10 +213,10 @@ export abstract class SymbolNavigationAction extends EditorAction { if (highlight) { const modelNow = targetEditor.getModel(); - const ids = targetEditor.deltaDecorations([], [{ range, options: { description: 'symbol-navigate-action-highlight', className: 'symbolHighlight' } }]); + const decorations = targetEditor.createDecorationsCollection([{ range, options: { description: 'symbol-navigate-action-highlight', className: 'symbolHighlight' } }]); setTimeout(() => { if (targetEditor.getModel() === modelNow) { - targetEditor.deltaDecorations(ids, []); + decorations.clear(); } }, 350); } diff --git a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts index 61ef2f26dd1..0efc185bd8b 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.ts @@ -17,7 +17,7 @@ import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { LocationLink } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; @@ -42,7 +42,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri private readonly editor: ICodeEditor; private readonly toUnhook = new DisposableStore(); private readonly toUnhookForKeyboard = new DisposableStore(); - private linkDecorations: string[] = []; + private readonly linkDecorations: IEditorDecorationsCollection; private currentWordAtPosition: IWordAtPosition | null = null; private previousPromise: CancelablePromise | null = null; @@ -53,6 +53,7 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, ) { this.editor = editor; + this.linkDecorations = this.editor.createDecorationsCollection(); let linkGesture = new ClickLinkGesture(editor); this.toUnhook.add(linkGesture); @@ -268,13 +269,11 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri } }; - this.linkDecorations = this.editor.deltaDecorations(this.linkDecorations, [newDecorations]); + this.linkDecorations.set([newDecorations]); } private removeLinkDecorations(): void { - if (this.linkDecorations.length > 0) { - this.linkDecorations = this.editor.deltaDecorations(this.linkDecorations, []); - } + this.linkDecorations.clear(); } private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): boolean { diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 82d21a37c2e..bf93d3b9d94 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -14,7 +14,7 @@ import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IConte import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IModelDecoration, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IModelDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { TokenizationRegistry } from 'vs/editor/common/languages'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/browser/hoverOperation'; @@ -25,8 +25,6 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/suggest'; import { AsyncIterableObject } from 'vs/base/common/async'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { Emitter } from 'vs/base/common/event'; -import { IModelDecorationsChangedEvent } from 'vs/editor/common/textModelEvents'; const $ = dom.$; @@ -34,7 +32,7 @@ export class ContentHoverController extends Disposable { private readonly _participants: IEditorHoverParticipant[]; private readonly _widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); - private readonly _decorationsChangerListener = this._register(new EditorDecorationsChangerListener(this._editor)); + private readonly _decorations = this._editor.createDecorationsCollection(); private readonly _computer: ContentHoverComputer; private readonly _hoverOperation: HoverOperation; @@ -64,7 +62,7 @@ export class ContentHoverController extends Disposable { this._register(this._hoverOperation.onResult((result) => { this._withResult(result.value, result.isComplete, result.hasLoadingMessage); })); - this._register(this._decorationsChangerListener.onDidChangeModelDecorations(() => this._onModelDecorationsChanged())); + this._register(this._decorations.onDidChange(() => this._onModelDecorationsChanged())); this._register(dom.addStandardDisposableListener(this._widget.getDomNode(), 'keydown', (e) => { if (e.equals(KeyCode.Escape)) { this.hide(); @@ -238,12 +236,12 @@ export class ContentHoverController extends Disposable { if (fragment.hasChildNodes()) { if (highlightRange) { - const highlightDecorations = this._decorationsChangerListener.deltaDecorations([], [{ + this._decorations.set([{ range: highlightRange, options: ContentHoverController._DECORATION_OPTIONS }]); disposables.add(toDisposable(() => { - this._decorationsChangerListener.deltaDecorations(highlightDecorations, []); + this._decorations.clear(); })); } @@ -266,38 +264,6 @@ export class ContentHoverController extends Disposable { }); } -/** - * Allows listening to `ICodeEditor.onDidChangeModelDecorations` and ignores the change caused by itself. - */ -class EditorDecorationsChangerListener extends Disposable { - - private readonly _onDidChangeModelDecorations = this._register(new Emitter()); - public readonly onDidChangeModelDecorations = this._onDidChangeModelDecorations.event; - - private _isChangingDecorations: boolean = false; - - constructor( - private readonly _editor: ICodeEditor - ) { - super(); - this._register(this._editor.onDidChangeModelDecorations((e) => { - if (this._isChangingDecorations) { - return; - } - this._onDidChangeModelDecorations.fire(e); - })); - } - - public deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] { - try { - this._isChangingDecorations = true; - return this._editor.deltaDecorations(oldDecorations, newDecorations); - } finally { - this._isChangingDecorations = false; - } - } -} - class ContentHoverVisibleData { constructor( public readonly colorPicker: IEditorHoverColorPickerWidget | null, diff --git a/src/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace.ts b/src/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace.ts index bdcc799b1ca..2ec112e6a3f 100644 --- a/src/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace.ts +++ b/src/vs/editor/contrib/inPlaceReplace/browser/inPlaceReplace.ts @@ -11,7 +11,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IInplaceReplaceSupportResult } from 'vs/editor/common/languages'; @@ -37,7 +37,7 @@ class InPlaceReplaceController implements IEditorContribution { private readonly editor: ICodeEditor; private readonly editorWorkerService: IEditorWorkerService; - private decorationIds: string[] = []; + private readonly decorations: IEditorDecorationsCollection; private currentRequest?: CancelablePromise; private decorationRemover?: CancelablePromise; @@ -47,6 +47,7 @@ class InPlaceReplaceController implements IEditorContribution { ) { this.editor = editor; this.editorWorkerService = editorWorkerService; + this.decorations = this.editor.createDecorationsCollection(); } public dispose(): void { @@ -114,7 +115,7 @@ class InPlaceReplaceController implements IEditorContribution { this.editor.pushUndoStop(); // add decoration - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, [{ + this.decorations.set([{ range: highlightRange, options: InPlaceReplaceController.DECORATION }]); @@ -124,7 +125,7 @@ class InPlaceReplaceController implements IEditorContribution { this.decorationRemover.cancel(); } this.decorationRemover = timeout(350); - this.decorationRemover.then(() => this.decorationIds = this.editor.deltaDecorations(this.decorationIds, [])).catch(onUnexpectedError); + this.decorationRemover.then(() => this.decorations.clear()).catch(onUnexpectedError); }).catch(onUnexpectedError); } diff --git a/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts index d08e327cfef..3b232c43005 100644 --- a/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts @@ -19,7 +19,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IModelDeltaDecoration, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -70,7 +70,9 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont private _currentRequestPosition: Position | null; private _currentRequestModelVersion: number | null; - private _currentDecorations: string[]; // The one at index 0 is the reference one + private _currentDecorations: IEditorDecorationsCollection; // The one at index 0 is the reference one + private _syncRangesToken: number = 0; + private _languageWordPattern: RegExp | null; private _currentWordPattern: RegExp | null; private _ignoreChangeEvent: boolean; @@ -91,7 +93,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); this._debounceInformation = languageFeatureDebounceService.for(this._providers, 'Linked Editing', { min: 200 }); - this._currentDecorations = []; + this._currentDecorations = this._editor.createDecorationsCollection(); this._languageWordPattern = null; this._currentWordPattern = null; this._ignoreChangeEvent = false; @@ -147,8 +149,8 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges(), this._debounceDuration ?? this._debounceInformation.get(model)); }; const rangeSyncScheduler = new Delayer(0); - const triggerRangeSync = (decorations: string[]) => { - this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(decorations)); + const triggerRangeSync = (token: number) => { + this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(token)); }; this._localToDispose.add(this._editor.onDidChangeCursorPosition(() => { triggerRangeUpdate(); @@ -156,9 +158,9 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont this._localToDispose.add(this._editor.onDidChangeModelContent((e) => { if (!this._ignoreChangeEvent) { if (this._currentDecorations.length > 0) { - const referenceRange = model.getDecorationRange(this._currentDecorations[0]); + const referenceRange = this._currentDecorations.getRange(0); if (referenceRange && e.changes.every(c => referenceRange.intersectRanges(c.range))) { - triggerRangeSync(this._currentDecorations); + triggerRangeSync(this._syncRangesToken); return; } } @@ -174,15 +176,15 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont this.updateRanges(); } - private _syncRanges(decorations: string[]): void { + private _syncRanges(token: number): void { // dalayed invocation, make sure we're still on - if (!this._editor.hasModel() || decorations !== this._currentDecorations || decorations.length === 0) { + if (!this._editor.hasModel() || token !== this._syncRangesToken || this._currentDecorations.length === 0) { // nothing to do return; } const model = this._editor.getModel(); - const referenceRange = model.getDecorationRange(decorations[0]); + const referenceRange = this._currentDecorations.getRange(0); if (!referenceRange || referenceRange.startLineNumber !== referenceRange.endLineNumber) { return this.clearRanges(); @@ -198,8 +200,8 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont } let edits: ISingleEditOperation[] = []; - for (let i = 1, len = decorations.length; i < len; i++) { - const mirrorRange = model.getDecorationRange(decorations[i]); + for (let i = 1, len = this._currentDecorations.length; i < len; i++) { + const mirrorRange = this._currentDecorations.getRange(i); if (!mirrorRange) { continue; } @@ -255,7 +257,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont public clearRanges(): void { this._visibleContextKey.set(false); - this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, []); + this._currentDecorations.clear(); if (this._currentRequest) { this._currentRequest.cancel(); this._currentRequest = null; @@ -290,8 +292,8 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont if (position.equals(this._currentRequestPosition)) { return; // same position } - if (this._currentDecorations && this._currentDecorations.length > 0) { - const range = model.getDecorationRange(this._currentDecorations[0]); + if (this._currentDecorations.length > 0) { + const range = this._currentDecorations.getRange(0); if (range && range.containsPosition(position)) { return; // just moving inside the existing primary range } @@ -341,7 +343,8 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: LinkedEditingContribution.DECORATION })); this._visibleContextKey.set(true); - this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); + this._currentDecorations.set(decorations); + this._syncRangesToken++; // cancel any pending syncRanges call } catch (err) { if (!isCancellationError(err)) { onUnexpectedError(err); diff --git a/src/vs/editor/contrib/multicursor/browser/multicursor.ts b/src/vs/editor/contrib/multicursor/browser/multicursor.ts index 27c7f171adf..f756d543b8f 100644 --- a/src/vs/editor/contrib/multicursor/browser/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/browser/multicursor.ts @@ -16,7 +16,7 @@ import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/comm import { CursorMoveCommands } from 'vs/editor/common/cursor/cursorMoveCommands'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { FindMatch, ITextModel, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -846,7 +846,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut private readonly editor: ICodeEditor; private _isEnabled: boolean; - private decorations: string[]; + private readonly _decorations: IEditorDecorationsCollection; private readonly updateSoon: RunOnceScheduler; private state: SelectionHighlighterState | null; @@ -857,7 +857,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut super(); this.editor = editor; this._isEnabled = editor.getOption(EditorOption.selectionHighlight); - this.decorations = []; + this._decorations = editor.createDecorationsCollection(); this.updateSoon = this._register(new RunOnceScheduler(() => this._update(), 300)); this.state = null; @@ -986,9 +986,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut this.state = newState; if (!this.state) { - this.editor.changeDecorations((accessor) => { - this.decorations = accessor.deltaDecorations(this.decorations, []); - }); + this._decorations.clear(); return; } @@ -1044,9 +1042,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut }; }); - this.editor.changeDecorations((accessor) => { - this.decorations = accessor.deltaDecorations(this.decorations, decorations); - }); + this._decorations.set(decorations); } private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index 37b4a750c6d..f226b74458d 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -142,22 +142,22 @@ export class SnippetController2 implements IEditorContribution { return undefined; } const { activeChoice } = this._session; - if (!activeChoice || activeChoice.options.length === 0) { + if (!activeChoice || activeChoice.choice.options.length === 0) { return undefined; } - const info = model.getWordUntilPosition(position); - const isAnyOfOptions = Boolean(activeChoice.options.find(o => o.value === info.word)); + const word = model.getValueInRange(activeChoice.range); + const isAnyOfOptions = Boolean(activeChoice.choice.options.find(o => o.value === word)); const suggestions: CompletionItem[] = []; - for (let i = 0; i < activeChoice.options.length; i++) { - const option = activeChoice.options[i]; + for (let i = 0; i < activeChoice.choice.options.length; i++) { + const option = activeChoice.choice.options[i]; suggestions.push({ kind: CompletionItemKind.Value, label: option.value, insertText: option.value, sortText: 'a'.repeat(i + 1), - range: new Range(position.lineNumber, info.startColumn, position.lineNumber, info.endColumn), - filterText: isAnyOfOptions ? `${info.word}_${option.value}` : undefined, + range: activeChoice.range, + filterText: isAnyOfOptions ? `${word}_${option.value}` : undefined, command: { id: 'jumpToNextSnippetPlaceholder', title: localize('next', 'Go to next placeholder...') } }); } @@ -223,8 +223,8 @@ export class SnippetController2 implements IEditorContribution { return; } - if (this._currentChoice !== activeChoice) { - this._currentChoice = activeChoice; + if (this._currentChoice !== activeChoice.choice) { + this._currentChoice = activeChoice.choice; // trigger suggest with the special choice completion provider queueMicrotask(() => { diff --git a/src/vs/editor/contrib/snippet/browser/snippetSession.ts b/src/vs/editor/contrib/snippet/browser/snippetSession.ts index 0d8fb8ada4a..b6345945756 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetSession.ts @@ -245,8 +245,23 @@ export class OneSnippet { return result; } - get activeChoice(): Choice | undefined { - return this._placeholderGroups[this._placeholderGroupsIdx][0].choice; + get activeChoice(): { choice: Choice; range: Range } | undefined { + if (!this._placeholderDecorations) { + return undefined; + } + const placeholder = this._placeholderGroups[this._placeholderGroupsIdx][0]; + if (!placeholder?.choice) { + return undefined; + } + const id = this._placeholderDecorations.get(placeholder); + if (!id) { + return undefined; + } + const range = this._editor.getModel().getDecorationRange(id); + if (!range) { + return undefined; + } + return { range, choice: placeholder.choice }; } get hasChoice(): boolean { @@ -628,7 +643,7 @@ export class SnippetSession { return this._snippets[0].hasChoice; } - get activeChoice(): Choice | undefined { + get activeChoice(): { choice: Choice; range: Range } | undefined { return this._snippets[0].activeChoice; } diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 326e6567180..44b682dea38 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -179,9 +179,9 @@ export class UnicodeHighlighter extends Disposable implements IEditorContributio } } - public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null { + public getDecorationInfo(decoration: IModelDecoration): UnicodeHighlighterDecorationInfo | null { if (this._highlighter) { - return this._highlighter.getDecorationInfo(decorationId); + return this._highlighter.getDecorationInfo(decoration); } return null; } @@ -214,7 +214,7 @@ function resolveOptions(trusted: boolean, options: InternalUnicodeHighlightOptio class DocumentUnicodeHighlighter extends Disposable { private readonly _model: ITextModel = this._editor.getModel(); private readonly _updateSoon: RunOnceScheduler; - private _decorationIds = new Set(); + private _decorations = this._editor.createDecorationsCollection(); constructor( private readonly _editor: IActiveCodeEditor, @@ -233,7 +233,7 @@ class DocumentUnicodeHighlighter extends Disposable { } public override dispose() { - this._decorationIds = new Set(this._model.deltaDecorations(Array.from(this._decorationIds), [])); + this._decorations.clear(); super.dispose(); } @@ -243,7 +243,7 @@ class DocumentUnicodeHighlighter extends Disposable { } if (!this._model.mightContainNonBasicASCII()) { - this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), [])); + this._decorations.clear(); return; } @@ -271,31 +271,21 @@ class DocumentUnicodeHighlighter extends Disposable { }); } } - this._decorationIds = new Set(this._editor.deltaDecorations( - Array.from(this._decorationIds), - decorations - )); + this._decorations.set(decorations); }); } - public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null { - if (!this._decorationIds.has(decorationId)) { + public getDecorationInfo(decoration: IModelDecoration): UnicodeHighlighterDecorationInfo | null { + if (!this._decorations.has(decoration)) { return null; } const model = this._editor.getModel(); - const range = model.getDecorationRange(decorationId)!; - const decoration = { - range: range, - options: Decorations.instance.getDecorationFromOptions(this._options), - id: decorationId, - ownerId: 0, - }; if ( !isModelDecorationVisible(model, decoration) ) { return null; } - const text = model.getValueInRange(range); + const text = model.getValueInRange(decoration.range); return { reason: computeReason(text, this._options)!, inComment: isModelDecorationInComment(model, decoration), @@ -308,7 +298,7 @@ class ViewportUnicodeHighlighter extends Disposable { private readonly _model: ITextModel = this._editor.getModel(); private readonly _updateSoon: RunOnceScheduler; - private _decorationIds = new Set(); + private readonly _decorations = this._editor.createDecorationsCollection(); constructor( private readonly _editor: IActiveCodeEditor, @@ -336,7 +326,7 @@ class ViewportUnicodeHighlighter extends Disposable { } public override dispose() { - this._decorationIds = new Set(this._model.deltaDecorations(Array.from(this._decorationIds), [])); + this._decorations.clear(); super.dispose(); } @@ -346,7 +336,7 @@ class ViewportUnicodeHighlighter extends Disposable { } if (!this._model.mightContainNonBasicASCII()) { - this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), [])); + this._decorations.clear(); return; } @@ -379,22 +369,15 @@ class ViewportUnicodeHighlighter extends Disposable { } this._updateState(totalResult); - this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), decorations)); + this._decorations.set(decorations); } - public getDecorationInfo(decorationId: string): UnicodeHighlighterDecorationInfo | null { - if (!this._decorationIds.has(decorationId)) { + public getDecorationInfo(decoration: IModelDecoration): UnicodeHighlighterDecorationInfo | null { + if (!this._decorations.has(decoration)) { return null; } const model = this._editor.getModel(); - const range = model.getDecorationRange(decorationId)!; - const text = model.getValueInRange(range); - const decoration = { - range: range, - options: Decorations.instance.getDecorationFromOptions(this._options), - id: decorationId, - ownerId: 0, - }; + const text = model.getValueInRange(decoration.range); if (!isModelDecorationVisible(model, decoration)) { return null; } @@ -449,7 +432,7 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa let index = 300; for (const d of lineDecorations) { - const highlightInfo = unicodeHighlighter.getDecorationInfo(d.id); + const highlightInfo = unicodeHighlighter.getDecorationInfo(d); if (!highlightInfo) { continue; } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index d2e3e3edd97..51c49f0cb1e 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -17,7 +17,7 @@ import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/commo import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -56,7 +56,7 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry; - isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean; + isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean; cancel(): void; } @@ -88,7 +88,7 @@ abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest return null; } - public isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean { + public isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean { const lineNumber = selection.startLineNumber; const startColumn = selection.startColumn; @@ -99,8 +99,8 @@ abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest // Even if we are on a different word, if that word is in the decorations ranges, the request is still valid // (Same symbol) - for (let i = 0, len = decorationIds.length; !requestIsValid && i < len; i++) { - let range = model.getDecorationRange(decorationIds[i]); + for (let i = 0, len = decorations.length; !requestIsValid && i < len; i++) { + const range = decorations.getRange(i); if (range && range.startLineNumber === lineNumber) { if (range.startColumn <= startColumn && range.endColumn >= endColumn) { requestIsValid = true; @@ -160,12 +160,12 @@ class TextualOccurenceAtPositionRequest extends OccurenceAtPositionRequest { }); } - public override isValid(model: ITextModel, selection: Selection, decorationIds: string[]): boolean { + public override isValid(model: ITextModel, selection: Selection, decorations: IEditorDecorationsCollection): boolean { const currentSelectionIsEmpty = selection.isEmpty(); if (this._selectionIsEmpty !== currentSelectionIsEmpty) { return false; } - return super.isValid(model, selection, decorationIds); + return super.isValid(model, selection, decorations); } } @@ -187,7 +187,7 @@ class WordHighlighter { private readonly providers: LanguageFeatureRegistry; private occurrencesHighlight: boolean; private readonly model: ITextModel; - private _decorationIds: string[]; + private readonly decorations: IEditorDecorationsCollection; private readonly toUnhook = new DisposableStore(); private workerRequestTokenId: number = 0; @@ -234,7 +234,7 @@ class WordHighlighter { } })); - this._decorationIds = []; + this.decorations = this.editor.createDecorationsCollection(); this.workerRequestTokenId = 0; this.workerRequest = null; this.workerRequestCompleted = false; @@ -244,7 +244,7 @@ class WordHighlighter { } public hasDecorations(): boolean { - return (this._decorationIds.length > 0); + return (this.decorations.length > 0); } public restore(): void { @@ -255,9 +255,8 @@ class WordHighlighter { } private _getSortedHighlights(): Range[] { - return arrays.coalesce( - this._decorationIds - .map((id) => this.model.getDecorationRange(id)) + return ( + this.decorations.getRanges() .sort(Range.compareRangesUsingStarts) ); } @@ -301,9 +300,9 @@ class WordHighlighter { } private _removeDecorations(): void { - if (this._decorationIds.length > 0) { + if (this.decorations.length > 0) { // remove decorations - this._decorationIds = this.editor.deltaDecorations(this._decorationIds, []); + this.decorations.clear(); this._hasWordHighlights.set(false); } } @@ -384,7 +383,7 @@ class WordHighlighter { // - 250ms later after the last cursor move event, render the occurrences // - no flickering! - const workerRequestIsValid = (this.workerRequest && this.workerRequest.isValid(this.model, editorSelection, this._decorationIds)); + const workerRequestIsValid = (this.workerRequest && this.workerRequest.isValid(this.model, editorSelection, this.decorations)); // There are 4 cases: // a) old workerRequest is valid & completed, renderDecorationsTimer fired @@ -453,7 +452,7 @@ class WordHighlighter { } } - this._decorationIds = this.editor.deltaDecorations(this._decorationIds, decorations); + this.decorations.set(decorations); this._hasWordHighlights.set(this.hasDecorations()); } diff --git a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts index c65d7f7e539..e3fbe97873e 100644 --- a/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts @@ -14,7 +14,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, IViewZone, IViewZo import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { ScrollType } from 'vs/editor/common/editorCommon'; +import { IEditorDecorationsCollection, ScrollType } from 'vs/editor/common/editorCommon'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -108,15 +108,13 @@ class Arrow { private static readonly _IdGenerator = new IdGenerator('.arrow-decoration-'); private readonly _ruleName = Arrow._IdGenerator.nextId(); - private _decorations: string[] = []; + private readonly _decorations = this._editor.createDecorationsCollection(); private _color: string | null = null; private _height: number = -1; constructor( private readonly _editor: ICodeEditor - ) { - // - } + ) { } dispose(): void { this.hide(); @@ -152,14 +150,18 @@ class Arrow { where = { lineNumber: where.lineNumber, column: 2 }; } - this._decorations = this._editor.deltaDecorations( - this._decorations, - [{ range: Range.fromPositions(where), options: { description: 'zone-widget-arrow', className: this._ruleName, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges } }] - ); + this._decorations.set([{ + range: Range.fromPositions(where), + options: { + description: 'zone-widget-arrow', + className: this._ruleName, + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }]); } hide(): void { - this._editor.deltaDecorations(this._decorations, []); + this._decorations.clear(); } } @@ -168,7 +170,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { private _arrow: Arrow | null = null; private _overlayWidget: OverlayWidgetDelegate | null = null; private _resizeSash: Sash | null = null; - private _positionMarkerId: string[] = []; + private readonly _positionMarkerId: IEditorDecorationsCollection; protected _viewZone: ViewZoneDelegate | null = null; protected readonly _disposables = new DisposableStore(); @@ -181,6 +183,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { constructor(editor: ICodeEditor, options: IOptions = {}) { this.editor = editor; + this._positionMarkerId = this.editor.createDecorationsCollection(); this.options = objects.deepClone(options); objects.mixin(this.options, defaultOptions, false); this.domNode = document.createElement('div'); @@ -212,8 +215,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { }); } - this.editor.deltaDecorations(this._positionMarkerId, []); - this._positionMarkerId = []; + this._positionMarkerId.clear(); this._disposables.dispose(); } @@ -291,17 +293,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { } get position(): Position | undefined { - const [id] = this._positionMarkerId; - if (!id) { - return undefined; - } - - const model = this.editor.getModel(); - if (!model) { - return undefined; - } - - const range = model.getDecorationRange(id); + const range = this._positionMarkerId.getRange(0); if (!range) { return undefined; } @@ -315,7 +307,7 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { this._isShowing = true; this._showImpl(range, heightInLines); this._isShowing = false; - this._positionMarkerId = this.editor.deltaDecorations(this._positionMarkerId, [{ range, options: ModelDecorationOptions.EMPTY }]); + this._positionMarkerId.set([{ range, options: ModelDecorationOptions.EMPTY }]); } hide(): void { diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index fdd3f3f8d7f..aba1e89aa1d 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -19,6 +19,7 @@ import 'vs/editor/contrib/comment/browser/comment'; import 'vs/editor/contrib/contextmenu/browser/contextmenu'; import 'vs/editor/contrib/cursorUndo/browser/cursorUndo'; import 'vs/editor/contrib/dnd/browser/dnd'; +import 'vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution'; import 'vs/editor/contrib/find/browser/findController'; import 'vs/editor/contrib/folding/browser/folding'; import 'vs/editor/contrib/fontZoom/browser/fontZoom'; diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index 53cba5e386e..e5070d8e3b5 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; -import { score } from 'vs/editor/common/languageSelector'; +import { LanguageSelector, score } from 'vs/editor/common/languageSelector'; suite('LanguageSelector', function () { @@ -15,18 +15,18 @@ suite('LanguageSelector', function () { }; test('score, invalid selector', function () { - assert.strictEqual(score({}, model.uri, model.language, true, undefined), 0); - assert.strictEqual(score(undefined!, model.uri, model.language, true, undefined), 0); - assert.strictEqual(score(null!, model.uri, model.language, true, undefined), 0); - assert.strictEqual(score('', model.uri, model.language, true, undefined), 0); + assert.strictEqual(score({}, model.uri, model.language, true, undefined, undefined), 0); + assert.strictEqual(score(undefined!, model.uri, model.language, true, undefined, undefined), 0); + assert.strictEqual(score(null!, model.uri, model.language, true, undefined, undefined), 0); + assert.strictEqual(score('', model.uri, model.language, true, undefined, undefined), 0); }); test('score, any language', function () { - assert.strictEqual(score({ language: '*' }, model.uri, model.language, true, undefined), 5); - assert.strictEqual(score('*', model.uri, model.language, true, undefined), 5); + assert.strictEqual(score({ language: '*' }, model.uri, model.language, true, undefined, undefined), 5); + assert.strictEqual(score('*', model.uri, model.language, true, undefined, undefined), 5); - assert.strictEqual(score('*', URI.parse('foo:bar'), model.language, true, undefined), 5); - assert.strictEqual(score('farboo', URI.parse('foo:bar'), model.language, true, undefined), 10); + assert.strictEqual(score('*', URI.parse('foo:bar'), model.language, true, undefined, undefined), 5); + assert.strictEqual(score('farboo', URI.parse('foo:bar'), model.language, true, undefined, undefined), 10); }); test('score, default schemes', function () { @@ -34,50 +34,50 @@ suite('LanguageSelector', function () { const uri = URI.parse('git:foo/file.txt'); const language = 'farboo'; - assert.strictEqual(score('*', uri, language, true, undefined), 5); - assert.strictEqual(score('farboo', uri, language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo', scheme: '' }, uri, language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo', scheme: 'git' }, uri, language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo', scheme: '*' }, uri, language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo' }, uri, language, true, undefined), 10); - assert.strictEqual(score({ language: '*' }, uri, language, true, undefined), 5); + assert.strictEqual(score('*', uri, language, true, undefined, undefined), 5); + assert.strictEqual(score('farboo', uri, language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '' }, uri, language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'git' }, uri, language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo', scheme: '*' }, uri, language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo' }, uri, language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: '*' }, uri, language, true, undefined, undefined), 5); - assert.strictEqual(score({ scheme: '*' }, uri, language, true, undefined), 5); - assert.strictEqual(score({ scheme: 'git' }, uri, language, true, undefined), 10); + assert.strictEqual(score({ scheme: '*' }, uri, language, true, undefined, undefined), 5); + assert.strictEqual(score({ scheme: 'git' }, uri, language, true, undefined, undefined), 10); }); test('score, filter', function () { - assert.strictEqual(score('farboo', model.uri, model.language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo' }, model.uri, model.language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true, undefined), 10); - assert.strictEqual(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true, undefined), 0); + assert.strictEqual(score('farboo', model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo' }, model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'file' }, model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score({ language: 'farboo', scheme: 'http' }, model.uri, model.language, true, undefined, undefined), 0); - assert.strictEqual(score({ pattern: '**/*.fb' }, model.uri, model.language, true, undefined), 10); - assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true, undefined), 10); - assert.strictEqual(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true, undefined), 0); - assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true, undefined), 0); + assert.strictEqual(score({ pattern: '**/*.fb' }, model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'file' }, model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score({ pattern: '**/*.fb' }, URI.parse('foo:bar'), model.language, true, undefined, undefined), 0); + assert.strictEqual(score({ pattern: '**/*.fb', scheme: 'foo' }, URI.parse('foo:bar'), model.language, true, undefined, undefined), 0); let doc = { uri: URI.parse('git:/my/file.js'), langId: 'javascript' }; - assert.strictEqual(score('javascript', doc.uri, doc.langId, true, undefined), 10); // 0; - assert.strictEqual(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true, undefined), 10); // 10; - assert.strictEqual(score('*', doc.uri, doc.langId, true, undefined), 5); // 5 - assert.strictEqual(score('fooLang', doc.uri, doc.langId, true, undefined), 0); // 0 - assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, true, undefined), 5); // 5 + assert.strictEqual(score('javascript', doc.uri, doc.langId, true, undefined, undefined), 10); // 0; + assert.strictEqual(score({ language: 'javascript', scheme: 'git' }, doc.uri, doc.langId, true, undefined, undefined), 10); // 10; + assert.strictEqual(score('*', doc.uri, doc.langId, true, undefined, undefined), 5); // 5 + assert.strictEqual(score('fooLang', doc.uri, doc.langId, true, undefined, undefined), 0); // 0 + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, true, undefined, undefined), 5); // 5 }); test('score, max(filters)', function () { let match = { language: 'farboo', scheme: 'file' }; let fail = { language: 'farboo', scheme: 'http' }; - assert.strictEqual(score(match, model.uri, model.language, true, undefined), 10); - assert.strictEqual(score(fail, model.uri, model.language, true, undefined), 0); - assert.strictEqual(score([match, fail], model.uri, model.language, true, undefined), 10); - assert.strictEqual(score([fail, fail], model.uri, model.language, true, undefined), 0); - assert.strictEqual(score(['farboo', '*'], model.uri, model.language, true, undefined), 10); - assert.strictEqual(score(['*', 'farboo'], model.uri, model.language, true, undefined), 10); + assert.strictEqual(score(match, model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score(fail, model.uri, model.language, true, undefined, undefined), 0); + assert.strictEqual(score([match, fail], model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score([fail, fail], model.uri, model.language, true, undefined, undefined), 0); + assert.strictEqual(score(['farboo', '*'], model.uri, model.language, true, undefined, undefined), 10); + assert.strictEqual(score(['*', 'farboo'], model.uri, model.language, true, undefined, undefined), 10); }); test('score hasAccessToAllModels', function () { @@ -85,30 +85,50 @@ suite('LanguageSelector', function () { uri: URI.parse('file:/my/file.js'), langId: 'javascript' }; - assert.strictEqual(score('javascript', doc.uri, doc.langId, false, undefined), 0); - assert.strictEqual(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false, undefined), 0); - assert.strictEqual(score('*', doc.uri, doc.langId, false, undefined), 0); - assert.strictEqual(score('fooLang', doc.uri, doc.langId, false, undefined), 0); - assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, false, undefined), 0); + assert.strictEqual(score('javascript', doc.uri, doc.langId, false, undefined, undefined), 0); + assert.strictEqual(score({ language: 'javascript', scheme: 'file' }, doc.uri, doc.langId, false, undefined, undefined), 0); + assert.strictEqual(score('*', doc.uri, doc.langId, false, undefined, undefined), 0); + assert.strictEqual(score('fooLang', doc.uri, doc.langId, false, undefined, undefined), 0); + assert.strictEqual(score(['fooLang', '*'], doc.uri, doc.langId, false, undefined, undefined), 0); - assert.strictEqual(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false, undefined), 10); - assert.strictEqual(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false, undefined), 5); + assert.strictEqual(score({ language: 'javascript', scheme: 'file', hasAccessToAllModels: true }, doc.uri, doc.langId, false, undefined, undefined), 10); + assert.strictEqual(score(['fooLang', '*', { language: '*', hasAccessToAllModels: true }], doc.uri, doc.langId, false, undefined, undefined), 5); }); test('score, notebookType', function () { let obj = { - uri: URI.parse('file:/my/file.js'), + uri: URI.parse('vscode-notebook-cell:///my/file.js#blabla'), langId: 'javascript', - notebookType: 'fooBook' + notebookType: 'fooBook', + notebookUri: URI.parse('file:///my/file.js') }; - assert.strictEqual(score('javascript', obj.uri, obj.langId, true, undefined), 10); - assert.strictEqual(score('javascript', obj.uri, obj.langId, true, obj.notebookType), 10); - assert.strictEqual(score({ notebookType: 'fooBook' }, obj.uri, obj.langId, true, obj.notebookType), 10); - assert.strictEqual(score({ notebookType: 'fooBook', language: 'javascript', scheme: 'file' }, obj.uri, obj.langId, true, obj.notebookType), 10); - assert.strictEqual(score({ notebookType: 'fooBook', language: '*' }, obj.uri, obj.langId, true, obj.notebookType), 10); - assert.strictEqual(score({ notebookType: '*', language: '*' }, obj.uri, obj.langId, true, obj.notebookType), 5); - assert.strictEqual(score({ notebookType: '*', language: 'javascript' }, obj.uri, obj.langId, true, obj.notebookType), 10); + assert.strictEqual(score('javascript', obj.uri, obj.langId, true, undefined, undefined), 10); + assert.strictEqual(score('javascript', obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 10); + assert.strictEqual(score({ notebookType: 'fooBook' }, obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 10); + assert.strictEqual(score({ notebookType: 'fooBook', language: 'javascript', scheme: 'file' }, obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 10); + assert.strictEqual(score({ notebookType: 'fooBook', language: '*' }, obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 10); + assert.strictEqual(score({ notebookType: '*', language: '*' }, obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 5); + assert.strictEqual(score({ notebookType: '*', language: 'javascript' }, obj.uri, obj.langId, true, obj.notebookUri, obj.notebookType), 10); + }); + + test('Snippet choices lost #149363', function () { + let selector: LanguageSelector = { + scheme: 'vscode-notebook-cell', + pattern: '/some/path/file.py', + language: 'python' + }; + + const modelUri = URI.parse('vscode-notebook-cell:///some/path/file.py'); + const nbUri = URI.parse('file:///some/path/file.py'); + assert.strictEqual(score(selector, modelUri, 'python', true, nbUri, 'jupyter'), 10); + + let selector2: LanguageSelector = { + ...selector, + notebookType: 'jupyter' + }; + + assert.strictEqual(score(selector2, modelUri, 'python', true, nbUri, 'jupyter'), 0); }); test('Document selector match - unexpected result value #60232', function () { @@ -117,7 +137,7 @@ suite('LanguageSelector', function () { scheme: 'file', pattern: '**/*.interface.json' }; - let value = score(selector, URI.parse('file:///C:/Users/zlhe/Desktop/test.interface.json'), 'json', true, undefined); + let value = score(selector, URI.parse('file:///C:/Users/zlhe/Desktop/test.interface.json'), 'json', true, undefined, undefined); assert.strictEqual(value, 10); }); @@ -128,7 +148,7 @@ suite('LanguageSelector', function () { pattern: '*.json' } }; - let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true, undefined); + let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true, undefined, undefined); assert.strictEqual(value, 10); }); @@ -141,13 +161,13 @@ suite('LanguageSelector', function () { let value = score({ language: 'bat', notebookType: 'xxx' - }, obj.uri, obj.langId, true, undefined); + }, obj.uri, obj.langId, true, undefined, undefined); assert.strictEqual(value, 0); value = score({ language: 'bat', notebookType: '*' - }, obj.uri, obj.langId, true, undefined); + }, obj.uri, obj.langId, true, undefined, undefined); assert.strictEqual(value, 0); }); }); diff --git a/src/vs/editor/test/common/services/modelService.test.ts b/src/vs/editor/test/common/services/modelService.test.ts index 6f5b396e05e..7cf81039b95 100644 --- a/src/vs/editor/test/common/services/modelService.test.ts +++ b/src/vs/editor/test/common/services/modelService.test.ts @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { CharCode } from 'vs/base/common/charCode'; +import { Event } from 'vs/base/common/event'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -495,6 +496,56 @@ suite('ModelSemanticColoring', () => { }); }); + test('issue #149412: VS Code hangs when bad semantic token data is received', async () => { + await runWithFakedTimers({}, async () => { + + disposables.add(languageService.registerLanguage({ id: 'testMode' })); + + let lastResult: SemanticTokens | SemanticTokensEdits | null = null; + + disposables.add(languageFeaturesService.documentSemanticTokensProvider.register('testMode', new class implements DocumentSemanticTokensProvider { + getLegend(): SemanticTokensLegend { + return { tokenTypes: ['class'], tokenModifiers: [] }; + } + async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise { + if (!lastResultId) { + // this is the first call + lastResult = { + resultId: '1', + data: new Uint32Array([4294967293, 0, 7, 16, 0, 1, 4, 3, 11, 1]) + }; + } else { + // this is the second call + lastResult = { + resultId: '2', + edits: [{ + start: 4294967276, + deleteCount: 0, + data: new Uint32Array([2, 0, 3, 11, 0]) + }] + }; + } + return lastResult; + } + releaseDocumentSemanticTokens(resultId: string | undefined): void { + } + })); + + const textModel = disposables.add(modelService.createModel('', languageService.createById('testMode'))); + + // wait for the semantic tokens to be fetched + await Event.toPromise(textModel.onDidChangeTokens); + assert.strictEqual(lastResult!.resultId, '1'); + + // edit the text + textModel.applyEdits([{ range: new Range(1, 1, 1, 1), text: 'foo' }]); + + // wait for the semantic tokens to be fetched again + await Event.toPromise(textModel.onDidChangeTokens); + assert.strictEqual(lastResult!.resultId, '2'); + }); + }); + test('DocumentSemanticTokens should be pick the token provider with actual items', async () => { await runWithFakedTimers({}, async () => { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 43215ef7ef7..ee1b68b4155 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2524,6 +2524,47 @@ declare namespace monaco.editor { * It is safe to call setModel(null) to simply detach the current model from the editor. */ setModel(model: IEditorModel | null): void; + /** + * Create a collection of decorations. All decorations added through this collection + * will get the ownerId of the editor (meaning they will not show up in other editors). + * These decorations will be automatically cleared when the editor's model changes. + */ + createDecorationsCollection(decorations?: IModelDeltaDecoration[]): IEditorDecorationsCollection; + } + + /** + * A collection of decorations + */ + export interface IEditorDecorationsCollection { + /** + * An event emitted when decorations change in the editor, + * but the change is not caused by us setting or clearing the collection. + */ + onDidChange: IEvent; + /** + * Get the decorations count. + */ + length: number; + /** + * Get the range for a decoration. + */ + getRange(index: number): Range | null; + /** + * Get all ranges for decorations. + */ + getRanges(): Range[]; + /** + * Determine if a decoration is in this collection. + */ + has(decoration: IModelDecoration): boolean; + /** + * Replace all previous decorations with `newDecorations`. + */ + set(newDecorations: IModelDeltaDecoration[]): void; + /** + * Remove all previous decorations. + */ + clear(): void; } /** @@ -3372,6 +3413,12 @@ declare namespace monaco.editor { * Configures bracket pair colorization (disabled by default). */ bracketPairColorization?: IBracketPairColorizationOptions; + /** + * Enables dropping into the editor from an external source. + * + * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. + */ + enableDropIntoEditor?: boolean; } export interface IDiffEditorBaseOptions { @@ -4273,107 +4320,108 @@ declare namespace monaco.editor { disableMonospaceOptimizations = 29, domReadOnly = 30, dragAndDrop = 31, - emptySelectionClipboard = 32, - extraEditorClassName = 33, - fastScrollSensitivity = 34, - find = 35, - fixedOverflowWidgets = 36, - folding = 37, - foldingStrategy = 38, - foldingHighlight = 39, - foldingImportsByDefault = 40, - foldingMaximumRegions = 41, - unfoldOnClickAfterEndOfLine = 42, - fontFamily = 43, - fontInfo = 44, - fontLigatures = 45, - fontSize = 46, - fontWeight = 47, - formatOnPaste = 48, - formatOnType = 49, - glyphMargin = 50, - gotoLocation = 51, - hideCursorInOverviewRuler = 52, - hover = 53, - inDiffEditor = 54, - inlineSuggest = 55, - letterSpacing = 56, - lightbulb = 57, - lineDecorationsWidth = 58, - lineHeight = 59, - lineNumbers = 60, - lineNumbersMinChars = 61, - linkedEditing = 62, - links = 63, - matchBrackets = 64, - minimap = 65, - mouseStyle = 66, - mouseWheelScrollSensitivity = 67, - mouseWheelZoom = 68, - multiCursorMergeOverlapping = 69, - multiCursorModifier = 70, - multiCursorPaste = 71, - occurrencesHighlight = 72, - overviewRulerBorder = 73, - overviewRulerLanes = 74, - padding = 75, - parameterHints = 76, - peekWidgetDefaultFocus = 77, - definitionLinkOpensInPeek = 78, - quickSuggestions = 79, - quickSuggestionsDelay = 80, - readOnly = 81, - renameOnType = 82, - renderControlCharacters = 83, - renderFinalNewline = 84, - renderLineHighlight = 85, - renderLineHighlightOnlyWhenFocus = 86, - renderValidationDecorations = 87, - renderWhitespace = 88, - revealHorizontalRightPadding = 89, - roundedSelection = 90, - rulers = 91, - scrollbar = 92, - scrollBeyondLastColumn = 93, - scrollBeyondLastLine = 94, - scrollPredominantAxis = 95, - selectionClipboard = 96, - selectionHighlight = 97, - selectOnLineNumbers = 98, - showFoldingControls = 99, - showUnused = 100, - snippetSuggestions = 101, - smartSelect = 102, - smoothScrolling = 103, - stickyTabStops = 104, - stopRenderingLineAfter = 105, - suggest = 106, - suggestFontSize = 107, - suggestLineHeight = 108, - suggestOnTriggerCharacters = 109, - suggestSelection = 110, - tabCompletion = 111, - tabIndex = 112, - unicodeHighlighting = 113, - unusualLineTerminators = 114, - useShadowDOM = 115, - useTabStops = 116, - wordSeparators = 117, - wordWrap = 118, - wordWrapBreakAfterCharacters = 119, - wordWrapBreakBeforeCharacters = 120, - wordWrapColumn = 121, - wordWrapOverride1 = 122, - wordWrapOverride2 = 123, - wrappingIndent = 124, - wrappingStrategy = 125, - showDeprecated = 126, - inlayHints = 127, - editorClassName = 128, - pixelRatio = 129, - tabFocusMode = 130, - layoutInfo = 131, - wrappingInfo = 132 + enableDropIntoEditor = 32, + emptySelectionClipboard = 33, + extraEditorClassName = 34, + fastScrollSensitivity = 35, + find = 36, + fixedOverflowWidgets = 37, + folding = 38, + foldingStrategy = 39, + foldingHighlight = 40, + foldingImportsByDefault = 41, + foldingMaximumRegions = 42, + unfoldOnClickAfterEndOfLine = 43, + fontFamily = 44, + fontInfo = 45, + fontLigatures = 46, + fontSize = 47, + fontWeight = 48, + formatOnPaste = 49, + formatOnType = 50, + glyphMargin = 51, + gotoLocation = 52, + hideCursorInOverviewRuler = 53, + hover = 54, + inDiffEditor = 55, + inlineSuggest = 56, + letterSpacing = 57, + lightbulb = 58, + lineDecorationsWidth = 59, + lineHeight = 60, + lineNumbers = 61, + lineNumbersMinChars = 62, + linkedEditing = 63, + links = 64, + matchBrackets = 65, + minimap = 66, + mouseStyle = 67, + mouseWheelScrollSensitivity = 68, + mouseWheelZoom = 69, + multiCursorMergeOverlapping = 70, + multiCursorModifier = 71, + multiCursorPaste = 72, + occurrencesHighlight = 73, + overviewRulerBorder = 74, + overviewRulerLanes = 75, + padding = 76, + parameterHints = 77, + peekWidgetDefaultFocus = 78, + definitionLinkOpensInPeek = 79, + quickSuggestions = 80, + quickSuggestionsDelay = 81, + readOnly = 82, + renameOnType = 83, + renderControlCharacters = 84, + renderFinalNewline = 85, + renderLineHighlight = 86, + renderLineHighlightOnlyWhenFocus = 87, + renderValidationDecorations = 88, + renderWhitespace = 89, + revealHorizontalRightPadding = 90, + roundedSelection = 91, + rulers = 92, + scrollbar = 93, + scrollBeyondLastColumn = 94, + scrollBeyondLastLine = 95, + scrollPredominantAxis = 96, + selectionClipboard = 97, + selectionHighlight = 98, + selectOnLineNumbers = 99, + showFoldingControls = 100, + showUnused = 101, + snippetSuggestions = 102, + smartSelect = 103, + smoothScrolling = 104, + stickyTabStops = 105, + stopRenderingLineAfter = 106, + suggest = 107, + suggestFontSize = 108, + suggestLineHeight = 109, + suggestOnTriggerCharacters = 110, + suggestSelection = 111, + tabCompletion = 112, + tabIndex = 113, + unicodeHighlighting = 114, + unusualLineTerminators = 115, + useShadowDOM = 116, + useTabStops = 117, + wordSeparators = 118, + wordWrap = 119, + wordWrapBreakAfterCharacters = 120, + wordWrapBreakBeforeCharacters = 121, + wordWrapColumn = 122, + wordWrapOverride1 = 123, + wordWrapOverride2 = 124, + wrappingIndent = 125, + wrappingStrategy = 126, + showDeprecated = 127, + inlayHints = 128, + editorClassName = 129, + pixelRatio = 130, + tabFocusMode = 131, + layoutInfo = 132, + wrappingInfo = 133 } export const EditorOptions: { @@ -4411,6 +4459,7 @@ declare namespace monaco.editor { domReadOnly: IEditorOption; dragAndDrop: IEditorOption; emptySelectionClipboard: IEditorOption; + enableDropIntoEditor: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; find: IEditorOption>>; @@ -4532,12 +4581,6 @@ declare namespace monaco.editor { * Defaults to an internal DOM node. */ overflowWidgetsDomNode?: HTMLElement; - /** - * Enables dropping into the editor. - * - * This shows a preview of the drop location and triggers an `onDropIntoEditor` event. - */ - enableDropIntoEditor?: boolean; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 6a05b051a40..2624e1cf366 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -25,6 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isDark } from 'vs/platform/theme/common/theme'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): IDisposable { const groups = menu.getActions(options); @@ -125,6 +126,7 @@ function fillInActions( export interface IMenuEntryActionViewItemOptions { draggable?: boolean; keybinding?: string; + hoverDelegate?: IHoverDelegate; } export class MenuEntryActionViewItem extends ActionViewItem { @@ -134,14 +136,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { private readonly _altKey: ModifierKeyEmitter; constructor( - _action: MenuItemAction, + action: MenuItemAction, options: IMenuEntryActionViewItemOptions | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @IThemeService protected _themeService: IThemeService ) { - super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding }); + super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -209,26 +211,24 @@ export class MenuEntryActionViewItem extends ActionViewItem { } override updateTooltip(): void { - if (this.label) { - const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id, this._contextKeyService); - const keybindingLabel = keybinding && keybinding.getLabel(); + const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id, this._contextKeyService); + const keybindingLabel = keybinding && keybinding.getLabel(); - const tooltip = this._commandAction.tooltip || this._commandAction.label; - let title = keybindingLabel - ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) - : tooltip; - if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) { - const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label; - const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService); - const altKeybindingLabel = altKeybinding && altKeybinding.getLabel(); - const altTitleSection = altKeybindingLabel - ? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel) - : altTooltip; - title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`; - } - this.label.title = title; - this.label.setAttribute('aria-label', title); + const tooltip = this._commandAction.tooltip || this._commandAction.label; + let title = keybindingLabel + ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) + : tooltip; + if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) { + const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label; + const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService); + const altKeybindingLabel = altKeybinding && altKeybinding.getLabel(); + const altTitleSection = altKeybindingLabel + ? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel) + : altTooltip; + + title = localize('titleAndKbAndAlt', "{0}\n[{1}] {2}", title, UILabelProvider.modifierLabels[OS].altKey, altTitleSection); } + this._applyUpdateTooltip(title); } override updateClass(): void { @@ -481,9 +481,9 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { /** * Creates action view items for menu actions or submenu actions. */ -export function createActionViewItem(instaService: IInstantiationService, action: IAction, options?: IDropdownMenuActionViewItemOptions): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem | BaseActionViewItem { +export function createActionViewItem(instaService: IInstantiationService, action: IAction, options?: IDropdownMenuActionViewItemOptions | IMenuEntryActionViewItemOptions): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem | BaseActionViewItem { if (action instanceof MenuItemAction) { - return instaService.createInstance(MenuEntryActionViewItem, action, undefined); + return instaService.createInstance(MenuEntryActionViewItem, action, options); } else if (action instanceof SubmenuItemAction) { if (action.item.rememberDefaultAction) { return instaService.createInstance(DropdownWithDefaultActionViewItem, action, options); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index e97c241b95c..e662dc4f73b 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -68,7 +68,6 @@ export class MenuId { static readonly ExtensionContext = new MenuId('ExtensionContext'); static readonly GlobalActivity = new MenuId('GlobalActivity'); static readonly TitleMenu = new MenuId('TitleMenu'); - static readonly TitleMenuQuickPick = new MenuId('TitleMenuQuickPick'); static readonly LayoutControlMenuSubmenu = new MenuId('LayoutControlMenuSubmenu'); static readonly LayoutControlMenu = new MenuId('LayoutControlMenu'); static readonly MenubarMainMenu = new MenuId('MenubarMainMenu'); diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.css b/src/vs/platform/contextview/browser/contextMenuHandler.css deleted file mode 100644 index 51a9e400923..00000000000 --- a/src/vs/platform/contextview/browser/contextMenuHandler.css +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.context-view .monaco-menu { - min-width: 130px; -} - diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index a0f444f666c..6ad90b3af61 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -10,7 +10,6 @@ import { Menu } from 'vs/base/browser/ui/menu/menu'; import { ActionRunner, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { isCancellationError } from 'vs/base/common/errors'; import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import 'vs/css!./contextMenuHandler'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts new file mode 100644 index 00000000000..14c6c8d11d3 --- /dev/null +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -0,0 +1,339 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DataTransfers } from 'vs/base/browser/dnd'; +import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; +import { coalesce } from 'vs/base/common/arrays'; +import { DeferredPromise } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { parse } from 'vs/base/common/marshalling'; +import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; +import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { ByteSize, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { extractSelection } from 'vs/platform/opener/common/opener'; +import { Registry } from 'vs/platform/registry/common/platform'; + +export interface FileAdditionalNativeProperties { + /** + * The real path to the file on the users filesystem. Only available on electron. + */ + readonly path?: string; +} + + +//#region Editor / Resources DND + +export const CodeDataTransfers = { + EDITORS: 'CodeEditors', + FILES: 'CodeFiles' +}; + +export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { + resource: URI | undefined; + + /** + * A hint that the source of the dragged editor input + * might not be the application but some external tool. + */ + isExternal?: boolean; + + /** + * Whether we probe for the dropped editor to be a workspace + * (i.e. code-workspace file or even a folder), allowing to + * open it as workspace instead of opening as editor. + */ + allowWorkspaceOpen?: boolean; +} + +export async function extractEditorsDropData(accessor: ServicesAccessor, e: DragEvent): Promise> { + const editors: IDraggedResourceEditorInput[] = []; + if (e.dataTransfer && e.dataTransfer.types.length > 0) { + + // Data Transfer: Code Editors + const rawEditorsData = e.dataTransfer.getData(CodeDataTransfers.EDITORS); + if (rawEditorsData) { + try { + editors.push(...parse(rawEditorsData)); + } catch (error) { + // Invalid transfer + } + } + + // Data Transfer: Resources + else { + try { + const rawResourcesData = e.dataTransfer.getData(DataTransfers.RESOURCES); + editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData)); + } catch (error) { + // Invalid transfer + } + } + + // Check for native file transfer + if (e.dataTransfer?.files) { + for (let i = 0; i < e.dataTransfer.files.length; i++) { + const file = e.dataTransfer.files[i]; + if (file && (file as FileAdditionalNativeProperties).path /* Electron only */) { + try { + editors.push({ resource: URI.file((file as FileAdditionalNativeProperties).path!), isExternal: true, allowWorkspaceOpen: true }); + } catch (error) { + // Invalid URI + } + } + } + } + + // Check for CodeFiles transfer + const rawCodeFiles = e.dataTransfer.getData(CodeDataTransfers.FILES); + if (rawCodeFiles) { + try { + const codeFiles: string[] = JSON.parse(rawCodeFiles); + for (const codeFile of codeFiles) { + editors.push({ resource: URI.file(codeFile), isExternal: true, allowWorkspaceOpen: true }); + } + } catch (error) { + // Invalid transfer + } + } + + // Web: Check for file transfer + if (isWeb && containsDragType(e, DataTransfers.FILES)) { + const files = e.dataTransfer.items; + if (files) { + const instantiationService = accessor.get(IInstantiationService); + const filesData = await instantiationService.invokeFunction(accessor => extractFilesDropData(accessor, e)); + for (const fileData of filesData) { + editors.push({ resource: fileData.resource, contents: fileData.contents?.toString(), isExternal: true, allowWorkspaceOpen: fileData.isDirectory }); + } + } + } + + // Workbench contributions + const contributions = Registry.as(Extensions.DragAndDropContribution).getAll(); + for (const contribution of contributions) { + const data = e.dataTransfer.getData(contribution.dataFormatKey); + if (data) { + try { + editors.push(...contribution.getEditorInputs(data)); + } catch (error) { + // Invalid transfer + } + } + } + } + + return editors; +} + +export function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | undefined): IDraggedResourceEditorInput[] { + const editors: IDraggedResourceEditorInput[] = []; + + if (rawResourcesData) { + const resourcesRaw: string[] = JSON.parse(rawResourcesData); + for (const resourceRaw of resourcesRaw) { + if (resourceRaw.indexOf(':') > 0) { // mitigate https://github.com/microsoft/vscode/issues/124946 + const { selection, uri } = extractSelection(URI.parse(resourceRaw)); + editors.push({ resource: uri, options: { selection } }); + } + } + } + + return editors; +} + + +interface IFileTransferData { + resource: URI; + isDirectory?: boolean; + contents?: VSBuffer; +} + +async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent): Promise { + + // Try to extract via `FileSystemHandle` + if (WebFileSystemAccess.supported(window)) { + const items = event.dataTransfer?.items; + if (items) { + return extractFileTransferData(accessor, items); + } + } + + // Try to extract via `FileList` + const files = event.dataTransfer?.files; + if (!files) { + return []; + } + + return extractFileListData(accessor, files); +} + +async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise { + const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file); + if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) { + return []; // only supported when running in web + } + + const results: DeferredPromise[] = []; + + for (let i = 0; i < items.length; i++) { + const file = items[i]; + if (file) { + const result = new DeferredPromise(); + results.push(result); + + (async () => { + try { + const handle = await file.getAsFileSystemHandle(); + if (!handle) { + result.complete(undefined); + return; + } + + if (WebFileSystemAccess.isFileSystemFileHandle(handle)) { + result.complete({ + resource: await fileSystemProvider.registerFileHandle(handle), + isDirectory: false + }); + } else if (WebFileSystemAccess.isFileSystemDirectoryHandle(handle)) { + result.complete({ + resource: await fileSystemProvider.registerDirectoryHandle(handle), + isDirectory: true + }); + } else { + result.complete(undefined); + } + } catch (error) { + result.complete(undefined); + } + })(); + } + } + + return coalesce(await Promise.all(results.map(result => result.p))); +} + +export async function extractFileListData(accessor: ServicesAccessor, files: FileList): Promise { + const dialogService = accessor.get(IDialogService); + + const results: DeferredPromise[] = []; + + for (let i = 0; i < files.length; i++) { + const file = files.item(i); + if (file) { + + // Skip for very large files because this operation is unbuffered + if (file.size > 100 * ByteSize.MB) { + dialogService.show(Severity.Warning, localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); + continue; + } + + const result = new DeferredPromise(); + results.push(result); + + const reader = new FileReader(); + + reader.onerror = () => result.complete(undefined); + reader.onabort = () => result.complete(undefined); + + reader.onload = async event => { + const name = file.name; + const loadResult = withNullAsUndefined(event.target?.result); + if (typeof name !== 'string' || typeof loadResult === 'undefined') { + result.complete(undefined); + return; + } + + result.complete({ + resource: URI.from({ scheme: Schemas.untitled, path: name }), + contents: typeof loadResult === 'string' ? VSBuffer.fromString(loadResult) : VSBuffer.wrap(new Uint8Array(loadResult)) + }); + }; + + // Start reading + reader.readAsArrayBuffer(file); + } + } + + return coalesce(await Promise.all(results.map(result => result.p))); +} + +//#endregion + +export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean { + if (!event.dataTransfer) { + return false; + } + + const dragTypes = event.dataTransfer.types; + const lowercaseDragTypes: string[] = []; + for (let i = 0; i < dragTypes.length; i++) { + lowercaseDragTypes.push(dragTypes[i].toLowerCase()); // somehow the types are lowercase + } + + for (const dragType of dragTypesToFind) { + if (lowercaseDragTypes.indexOf(dragType.toLowerCase()) >= 0) { + return true; + } + } + + return false; +} + +//#region DND contributions + +export interface IResourceStat { + resource: URI; + isDirectory?: boolean; +} + +export interface IDragAndDropContributionRegistry { + /** + * Registers a drag and drop contribution. + */ + register(contribution: IDragAndDropContribution): void; + + /** + * Returns all registered drag and drop contributions. + */ + getAll(): IterableIterator; +} + +export interface IDragAndDropContribution { + readonly dataFormatKey: string; + getEditorInputs(data: string): IDraggedResourceEditorInput[]; + setData(resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; +} + +class DragAndDropContributionRegistry implements IDragAndDropContributionRegistry { + private readonly _contributions = new Map(); + + register(contribution: IDragAndDropContribution): void { + if (this._contributions.has(contribution.dataFormatKey)) { + throw new Error(`A drag and drop contributiont with key '${contribution.dataFormatKey}' was already registered.`); + } + this._contributions.set(contribution.dataFormatKey, contribution); + } + + getAll(): IterableIterator { + return this._contributions.values(); + } +} + +export const Extensions = { + DragAndDropContribution: 'workbench.contributions.dragAndDrop' +}; + +Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry()); + +//#endregion diff --git a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts index 81fa42981c2..a73af96a7c8 100644 --- a/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts +++ b/src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts @@ -10,6 +10,7 @@ import { CancellationError, getErrorMessage } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; +import { isBoolean } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { @@ -363,8 +364,9 @@ export abstract class AbstractExtensionManagementService extends Disposable impl throw new ExtensionManagementError(nls.localize('malicious extension', "Can't install '{0}' extension since it was reported to be problematic.", extension.identifier.id), ExtensionManagementErrorCode.Malicious); } - if (!!report.unsupportedPreReleaseExtensions && !!report.unsupportedPreReleaseExtensions[extension.identifier.id]) { - throw new ExtensionManagementError(nls.localize('unsupported prerelease extension', "Can't install '{0}' extension because it is no longer supported. It is now part of the '{1}' extension as a pre-release version.", extension.identifier.id, report.unsupportedPreReleaseExtensions[extension.identifier.id].displayName), ExtensionManagementErrorCode.UnsupportedPreRelease); + const deprecated = report.deprecated ? report.deprecated[extension.identifier.id.toLowerCase()] : undefined; + if (deprecated && !isBoolean(deprecated)) { + throw new ExtensionManagementError(nls.localize('unsupported prerelease extension', "Can't install '{0}' extension because it is deprecated. Use '{1}' extension instead.", extension.identifier.id, deprecated.displayName), ExtensionManagementErrorCode.UnsupportedPreRelease); } if (!await this.canInstall(extension)) { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index eba50f040e4..b75a76752b8 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -535,11 +535,9 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller }; } -type PreReleaseMigrationInfo = { id: string; displayName: string; migrateStorage?: boolean; engine?: string }; interface IRawExtensionsControlManifest { malicious: string[]; - unsupported?: IStringDictionary; - migrateToPreRelease?: IStringDictionary; + deprecated?: IStringDictionary; } abstract class AbstractExtensionGalleryService implements IExtensionGalleryService { @@ -1150,30 +1148,13 @@ abstract class AbstractExtensionGalleryService implements IExtensionGalleryServi const result = await asJson(context); const malicious: IExtensionIdentifier[] = []; - const unsupportedPreReleaseExtensions: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }> = {}; - if (result) { for (const id of result.malicious) { malicious.push({ id }); } - if (result.unsupported) { - for (const extensionId of Object.keys(result.unsupported)) { - const value = result.unsupported[extensionId]; - if (!isBoolean(value)) { - unsupportedPreReleaseExtensions[extensionId.toLowerCase()] = value.preReleaseExtension; - } - } - } - if (result.migrateToPreRelease) { - for (const [unsupportedPreReleaseExtensionId, preReleaseExtensionInfo] of Object.entries(result.migrateToPreRelease)) { - if (!preReleaseExtensionInfo.engine || isEngineValid(preReleaseExtensionInfo.engine, this.productService.version, this.productService.date)) { - unsupportedPreReleaseExtensions[unsupportedPreReleaseExtensionId.toLowerCase()] = preReleaseExtensionInfo; - } - } - } } - return { malicious, unsupportedPreReleaseExtensions }; + return { malicious, deprecated: result?.deprecated }; } } diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index b28421b4bf1..061de6580a9 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -284,7 +284,7 @@ export const enum StatisticType { export interface IExtensionsControlManifest { malicious: IExtensionIdentifier[]; - unsupportedPreReleaseExtensions?: IStringDictionary<{ id: string; displayName: string; migrateStorage?: boolean }>; + deprecated?: IStringDictionary; } export const enum InstallOperation { diff --git a/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts b/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts index 15ea0319efc..87eaf1447a6 100644 --- a/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts +++ b/src/vs/platform/extensionManagement/common/unsupportedExtensionsMigration.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { isBoolean } from 'vs/base/common/types'; import { IExtensionGalleryService, IExtensionManagementService, IGlobalExtensionEnablementService, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; import { areSameExtensions, getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IExtensionStorageService } from 'vs/platform/extensionManagement/common/extensionStorage'; @@ -20,18 +21,22 @@ import { ILogService } from 'vs/platform/log/common/log'; export async function migrateUnsupportedExtensions(extensionManagementService: IExtensionManagementService, galleryService: IExtensionGalleryService, extensionStorageService: IExtensionStorageService, extensionEnablementService: IGlobalExtensionEnablementService, logService: ILogService): Promise { try { const extensionsControlManifest = await extensionManagementService.getExtensionsControlManifest(); - if (!extensionsControlManifest.unsupportedPreReleaseExtensions) { + if (!extensionsControlManifest.deprecated) { return; } const installed = await extensionManagementService.getInstalled(ExtensionType.User); - for (const [unsupportedExtensionId, { id: preReleaseExtensionId, migrateStorage }] of Object.entries(extensionsControlManifest.unsupportedPreReleaseExtensions)) { + for (const [unsupportedExtensionId, deprecated] of Object.entries(extensionsControlManifest.deprecated)) { + if (isBoolean(deprecated)) { + continue; + } + const { id: preReleaseExtensionId, migrateStorage, preRelease } = deprecated; const unsupportedExtension = installed.find(i => areSameExtensions(i.identifier, { id: unsupportedExtensionId })); // Unsupported Extension is not installed if (!unsupportedExtension) { continue; } - const gallery = (await galleryService.getExtensions([{ id: preReleaseExtensionId, preRelease: true }], { targetPlatform: await extensionManagementService.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; + const gallery = (await galleryService.getExtensions([{ id: preReleaseExtensionId, preRelease }], { targetPlatform: await extensionManagementService.getTargetPlatform(), compatible: true }, CancellationToken.None))[0]; if (!gallery) { logService.info(`Skipping migrating '${unsupportedExtension.identifier.id}' extension because, the comaptible target '${preReleaseExtensionId}' extension is not found`); continue; diff --git a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts similarity index 87% rename from src/vs/platform/files/test/browser/indexedDBFileService.test.ts rename to src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts index 3387e3b989b..a12b43189f0 100644 --- a/src/vs/platform/files/test/browser/indexedDBFileService.test.ts +++ b/src/vs/platform/files/test/browser/indexedDBFileService.integrationTest.ts @@ -12,20 +12,16 @@ import { basename, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { flakySuite } from 'vs/base/test/common/testUtils'; import { IndexedDBFileSystemProvider } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; -import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileStatWithMetadata } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileSystemProviderError, FileSystemProviderErrorCode, FileType } from 'vs/platform/files/common/files'; import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; flakySuite('IndexedDBFileSystemProvider', function () { - const logSchema = 'logs'; - let service: FileService; - let logFileProvider: IndexedDBFileSystemProvider; let userdataFileProvider: IndexedDBFileSystemProvider; const testDir = '/'; - const logfileURIFromPaths = (paths: string[]) => joinPath(URI.from({ scheme: logSchema, path: testDir }), ...paths); const userdataURIFromPaths = (paths: readonly string[]) => joinPath(URI.from({ scheme: Schemas.vscodeUserData, path: testDir }), ...paths); const disposables = new DisposableStore(); @@ -69,10 +65,6 @@ flakySuite('IndexedDBFileSystemProvider', function () { const indexedDB = await IndexedDB.create('vscode-web-db-test', 1, ['vscode-userdata-store', 'vscode-logs-store']); - logFileProvider = new IndexedDBFileSystemProvider(logSchema, indexedDB, 'vscode-logs-store', false); - disposables.add(service.registerProvider(logSchema, logFileProvider)); - disposables.add(logFileProvider); - userdataFileProvider = new IndexedDBFileSystemProvider(Schemas.vscodeUserData, indexedDB, 'vscode-userdata-store', true); disposables.add(service.registerProvider(Schemas.vscodeUserData, userdataFileProvider)); disposables.add(userdataFileProvider); @@ -84,8 +76,7 @@ flakySuite('IndexedDBFileSystemProvider', function () { }); teardown(async () => { - await logFileProvider.delete(logfileURIFromPaths([]), { recursive: true, useTrash: false }); - await userdataFileProvider.delete(userdataURIFromPaths([]), { recursive: true, useTrash: false }); + await userdataFileProvider.reset(); disposables.clear(); }); @@ -234,60 +225,43 @@ flakySuite('IndexedDBFileSystemProvider', function () { assert.strictEqual(event!.target!.resource.path, resource.path); } - const makeBatchTester = (size: number, name: string) => { + const fileCreateBatchTester = (size: number, name: string) => { const batch = Array.from({ length: size }).map((_, i) => ({ contents: `Hello${i}`, resource: userdataURIFromPaths(['batched', name, `Hello${i}.txt`]) })); - let stats: Promise | undefined = undefined; + let creationPromises: Promise | undefined = undefined; return { async create() { - return stats = Promise.all(batch.map(entry => service.createFile(entry.resource, VSBuffer.fromString(entry.contents)))); + return creationPromises = Promise.all(batch.map(entry => userdataFileProvider.writeFile(entry.resource, VSBuffer.fromString(entry.contents).buffer, { create: true, overwrite: true, unlock: false }))); }, async assertContentsCorrect() { + if (!creationPromises) { throw Error('read called before create'); } + await creationPromises; await Promise.all(batch.map(async (entry, i) => { - if (!stats) { throw Error('read called before create'); } - const stat = (await stats!)[i]; - assert.strictEqual(stat.name, `Hello${i}.txt`); - assert.strictEqual((await userdataFileProvider.stat(stat.resource)).type, FileType.File); - assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(stat.resource)), entry.contents); - })); - }, - async delete() { - await service.del(userdataURIFromPaths(['batched', name]), { recursive: true, useTrash: false }); - }, - async assertContentsEmpty() { - if (!stats) { throw Error('assertContentsEmpty called before create'); } - await Promise.all((await stats).map(async stat => { - const newStat = await userdataFileProvider.stat(stat.resource).catch(e => e.code); - assert.strictEqual(newStat, FileSystemProviderErrorCode.FileNotFound); + assert.strictEqual((await userdataFileProvider.stat(entry.resource)).type, FileType.File); + assert.strictEqual(new TextDecoder().decode(await userdataFileProvider.readFile(entry.resource)), entry.contents); })); } }; }; - test('createFile (small batch)', async () => { - const tester = makeBatchTester(50, 'smallBatch'); + test('createFile - batch', async () => { + const tester = fileCreateBatchTester(20, 'batch'); await tester.create(); await tester.assertContentsCorrect(); - await tester.delete(); - await tester.assertContentsEmpty(); }); - test('createFile (mixed parallel/sequential)', async () => { - const single1 = makeBatchTester(1, 'single1'); - const single2 = makeBatchTester(1, 'single2'); + test('createFile - batch (mixed parallel/sequential)', async () => { + const batch1 = fileCreateBatchTester(1, 'batch1'); + const batch2 = fileCreateBatchTester(20, 'batch2'); + const batch3 = fileCreateBatchTester(1, 'batch3'); + const batch4 = fileCreateBatchTester(20, 'batch4'); - const batch1 = makeBatchTester(20, 'batch1'); - const batch2 = makeBatchTester(20, 'batch2'); - - single1.create(); batch1.create(); - await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); - single2.create(); batch2.create(); - await Promise.all([single2.assertContentsCorrect(), batch2.assertContentsCorrect()]); - await Promise.all([single1.assertContentsCorrect(), batch1.assertContentsCorrect()]); - - await (Promise.all([single1.delete(), single2.delete(), batch1.delete(), batch2.delete()])); - await (Promise.all([single1.assertContentsEmpty(), single2.assertContentsEmpty(), batch1.assertContentsEmpty(), batch2.assertContentsEmpty()])); + await Promise.all([batch1.assertContentsCorrect(), batch2.assertContentsCorrect()]); + batch3.create(); + batch4.create(); + await Promise.all([batch3.assertContentsCorrect(), batch4.assertContentsCorrect()]); + await Promise.all([batch1.assertContentsCorrect(), batch2.assertContentsCorrect()]); }); test('rename not existing resource', async () => { @@ -392,10 +366,12 @@ flakySuite('IndexedDBFileSystemProvider', function () { test('rename to an existing file with overwrite', async () => { const parent = await service.resolve(userdataURIFromPaths([])); const sourceFile = joinPath(parent.resource, 'sourceFile'); - await service.writeFile(sourceFile, VSBuffer.fromString('This is source file')); - const targetFile = joinPath(parent.resource, 'targetFile'); - await service.writeFile(targetFile, VSBuffer.fromString('This is target file')); + + await Promise.all([ + service.writeFile(sourceFile, VSBuffer.fromString('This is source file')), + service.writeFile(targetFile, VSBuffer.fromString('This is target file')) + ]); await service.move(sourceFile, targetFile, true); @@ -434,11 +410,14 @@ flakySuite('IndexedDBFileSystemProvider', function () { const sourceFolder = joinPath(parent.resource, 'sourceFolder'); const sourceFile1 = joinPath(sourceFolder, 'folder1', 'file1'); - await service.writeFile(sourceFile1, VSBuffer.fromString('Source File 1')); const sourceFile2 = joinPath(sourceFolder, 'folder2', 'file1'); - await service.writeFile(sourceFile2, VSBuffer.fromString('Source File 2')); const sourceEmptyFolder = joinPath(sourceFolder, 'folder3'); - await service.createFolder(sourceEmptyFolder); + + await Promise.all([ + service.writeFile(sourceFile1, VSBuffer.fromString('Source File 1')), + service.writeFile(sourceFile2, VSBuffer.fromString('Source File 2')), + service.createFolder(sourceEmptyFolder) + ]); const targetFolder = joinPath(parent.resource, 'targetFolder'); const targetFile1 = joinPath(targetFolder, 'folder1', 'file1'); @@ -459,14 +438,17 @@ flakySuite('IndexedDBFileSystemProvider', function () { const sourceFolder = joinPath(parent.resource, 'sourceFolder'); const sourceFile1 = joinPath(sourceFolder, 'folder1', 'file1'); - await service.writeFile(sourceFile1, VSBuffer.fromString('Source File 1')); const targetFolder = joinPath(parent.resource, 'targetFolder'); const targetFile1 = joinPath(targetFolder, 'folder1', 'file1'); const targetFile2 = joinPath(targetFolder, 'folder1', 'file2'); - await service.writeFile(targetFile2, VSBuffer.fromString('Target File 2')); const targetFile3 = joinPath(targetFolder, 'folder2', 'file1'); - await service.writeFile(targetFile3, VSBuffer.fromString('Target File 3')); + + await Promise.all([ + service.writeFile(sourceFile1, VSBuffer.fromString('Source File 1')), + service.writeFile(targetFile2, VSBuffer.fromString('Target File 2')), + service.writeFile(targetFile3, VSBuffer.fromString('Target File 3')) + ]); await service.move(sourceFolder, targetFolder, true); diff --git a/src/vs/platform/telemetry/common/errorTelemetry.ts b/src/vs/platform/telemetry/common/errorTelemetry.ts index 309b372c83b..acc2dbd0833 100644 --- a/src/vs/platform/telemetry/common/errorTelemetry.ts +++ b/src/vs/platform/telemetry/common/errorTelemetry.ts @@ -10,14 +10,16 @@ import { safeStringify } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; type ErrorEventFragment = { - callstack: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; - msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; - file?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; - line?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - column?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; - uncaught_error_name?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; - uncaught_error_msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth' }; - count?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true }; + owner: 'lramos15, sbatten'; + comment: 'Whenever an error in VS Code is thrown.'; + callstack: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The callstack of the error.' }; + msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The message of the error. Normally the first line int the callstack.' }; + file?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'The file the error originated from.' }; + line?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The line the error originate on.' }; + column?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The column of the line which the error orginated on.' }; + uncaught_error_name?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'If the error is uncaught what is the error type' }; + uncaught_error_msg?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; comment: 'If the error is uncaught this is just msg but for uncaught errors.' }; + count?: { classification: 'CallstackOrException'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'How many times this error has been thrown' }; }; export interface ErrorEvent { callstack: string; diff --git a/src/vs/platform/telemetry/common/remoteTelemetryChannel.ts b/src/vs/platform/telemetry/common/remoteTelemetryChannel.ts index 66bd712a808..97fb3ec4989 100644 --- a/src/vs/platform/telemetry/common/remoteTelemetryChannel.ts +++ b/src/vs/platform/telemetry/common/remoteTelemetryChannel.ts @@ -45,6 +45,10 @@ export class ServerTelemetryChannel extends Disposable implements IServerChannel return Promise.resolve(); } + + case 'ping': { + return; + } } // Command we cannot handle so we throw an error throw new Error(`IPC Command ${command} not found`); diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index c3d3657b6a9..91b3244bd3f 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -209,7 +209,8 @@ export const enum ProcessPropertyType { ShellType = 'shellType', HasChildProcesses = 'hasChildProcesses', ResolvedShellLaunchConfig = 'resolvedShellLaunchConfig', - OverrideDimensions = 'overrideDimensions' + OverrideDimensions = 'overrideDimensions', + FailedShellIntegrationActivation = 'failedShellIntegrationActivation' } export interface IProcessProperty { @@ -226,6 +227,7 @@ export interface IProcessPropertyMap { [ProcessPropertyType.HasChildProcesses]: boolean; [ProcessPropertyType.ResolvedShellLaunchConfig]: IShellLaunchConfig; [ProcessPropertyType.OverrideDimensions]: ITerminalDimensionsOverride | undefined; + [ProcessPropertyType.FailedShellIntegrationActivation]: boolean | undefined; } export interface IFixedTerminalDimensions { diff --git a/src/vs/platform/terminal/common/terminalProfiles.ts b/src/vs/platform/terminal/common/terminalProfiles.ts index 37b166d82c1..afaac9b4da8 100644 --- a/src/vs/platform/terminal/common/terminalProfiles.ts +++ b/src/vs/platform/terminal/common/terminalProfiles.ts @@ -59,7 +59,7 @@ function createProfileDescription(profile: ITerminalProfile): string { } function createExtensionProfileDescription(profile: IExtensionTerminalProfile): string { - let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.title}\n- extensionIdenfifier: ${profile.extensionIdentifier}`; + let description = `$(${ThemeIcon.isThemeIcon(profile.icon) ? profile.icon.id : profile.icon ? profile.icon : Codicon.terminal.id}) ${profile.title}\n- extensionIdentifier: ${profile.extensionIdentifier}`; return description; } diff --git a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts index ebe5ab92d14..df36e27041d 100644 --- a/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts +++ b/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts @@ -15,6 +15,7 @@ import { ILogService } from 'vs/platform/log/common/log'; // eslint-disable-next-line code-import-patterns import type { ITerminalAddon, Terminal } from 'xterm-headless'; import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; /** * Shell integration is a feature that enhances the terminal's understanding of what's happening @@ -123,8 +124,11 @@ const enum VSCodeOscPt { export class ShellIntegrationAddon extends Disposable implements IShellIntegration, ITerminalAddon { private _terminal?: Terminal; readonly capabilities = new TerminalCapabilityStore(); + private _hasUpdatedTelemetry: boolean = false; + private _activationTimeout: any; constructor( + private readonly _telemetryService: ITelemetryService | undefined, @ILogService private readonly _logService: ILogService ) { super(); @@ -134,9 +138,23 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati this._terminal = xterm; this.capabilities.add(TerminalCapability.PartialCommandDetection, new PartialCommandDetectionCapability(this._terminal)); this._register(xterm.parser.registerOscHandler(ShellIntegrationOscPs.VSCode, data => this._handleVSCodeSequence(data))); + this._ensureCapabilitiesOrAddFailureTelemetry(); } private _handleVSCodeSequence(data: string): boolean { + const didHandle = this._doHandleVSCodeSequence(data); + if (!this._hasUpdatedTelemetry && didHandle) { + this._telemetryService?.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationActivationSucceeded'); + this._hasUpdatedTelemetry = true; + if (this._activationTimeout !== undefined) { + clearTimeout(this._activationTimeout); + this._activationTimeout = undefined; + } + } + return didHandle; + } + + private _doHandleVSCodeSequence(data: string): boolean { if (!this._terminal) { return false; } @@ -211,6 +229,16 @@ export class ShellIntegrationAddon extends Disposable implements IShellIntegrati return false; } + private async _ensureCapabilitiesOrAddFailureTelemetry(): Promise { + this._activationTimeout = setTimeout(() => { + if (!this.capabilities.get(TerminalCapability.CommandDetection) && !this.capabilities.get(TerminalCapability.CwdDetection)) { + this._telemetryService?.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationActivationTimeout'); + this._logService.warn('Shell integration failed to add capabilities within 10 seconds'); + } + this._hasUpdatedTelemetry = true; + }, 10000); + } + serialize(): ISerializedCommandDetectionCapability { if (!this._terminal || !this.capabilities.has(TerminalCapability.CommandDetection)) { return { diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index 0b4301c23e6..a3191a3230c 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -742,7 +742,7 @@ class XtermSerializer implements ITerminalSerializer { this._xterm.writeln(reviveBuffer); } this.setUnicodeVersion(unicodeVersion); - this._shellIntegrationAddon = new ShellIntegrationAddon(logService); + this._shellIntegrationAddon = new ShellIntegrationAddon(undefined, logService); this._xterm.loadAddon(this._shellIntegrationAddon); } diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index a12bb5879dd..bc809abf0cd 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -97,7 +97,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess shellType: undefined, hasChildProcesses: true, resolvedShellLaunchConfig: {}, - overrideDimensions: undefined + overrideDimensions: undefined, + failedShellIntegrationActivation: false }; private static _lastKillOrStart = 0; private _exitCode: number | undefined; @@ -213,6 +214,8 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess await fs.copyFile(f.source, f.dest); } } + } else { + this._onDidChangeProperty.fire({ type: ProcessPropertyType.FailedShellIntegrationActivation, value: true }); } } diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 6e163e27d16..4696b6036e6 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -470,7 +470,7 @@ export const menuBackground = registerColor('menu.background', { dark: selectBac export const menuSelectionForeground = registerColor('menu.selectionForeground', { dark: listActiveSelectionForeground, light: listActiveSelectionForeground, hcDark: listActiveSelectionForeground, hcLight: listActiveSelectionForeground }, nls.localize('menuSelectionForeground', "Foreground color of the selected menu item in menus.")); export const menuSelectionBackground = registerColor('menu.selectionBackground', { dark: listActiveSelectionBackground, light: listActiveSelectionBackground, hcDark: listActiveSelectionBackground, hcLight: listActiveSelectionBackground }, nls.localize('menuSelectionBackground', "Background color of the selected menu item in menus.")); export const menuSelectionBorder = registerColor('menu.selectionBorder', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('menuSelectionBorder', "Border color of the selected menu item in menus.")); -export const menuSeparatorBackground = registerColor('menu.separatorBackground', { dark: '#BBBBBB', light: '#888888', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); +export const menuSeparatorBackground = registerColor('menu.separatorBackground', { dark: '#606060', light: '#D4D4D4', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('menuSeparatorBackground', "Color of a separator menu item in menus.")); /** * Toolbar colors diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 6657810f7d6..134fb2e963b 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { joinPath } from 'vs/base/common/resources'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; @@ -31,401 +32,429 @@ suite('UserDataAutoSyncService', () => { teardown(() => disposableStore.clear()); test('test auto sync with sync resource change triggers sync', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); - // Sync once and reset requests - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - target.reset(); + // Sync once and reset requests + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + target.reset(); - const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Trigger auto sync with settings change - await testObject.triggerSync([SyncResource.Settings], false, false); + // Trigger auto sync with settings change + await testObject.triggerSync([SyncResource.Settings], false, false); - // Filter out machine requests - const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); + // Filter out machine requests + const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); - // Make sure only one manifest request is made - assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + // Make sure only one manifest request is made + assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); }); test('test auto sync with sync resource change triggers sync for every change', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); - // Sync once and reset requests - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - target.reset(); + // Sync once and reset requests + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + target.reset(); - const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Trigger auto sync with settings change multiple times - for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync([SyncResource.Settings], false, false); - } + // Trigger auto sync with settings change multiple times + for (let counter = 0; counter < 2; counter++) { + await testObject.triggerSync([SyncResource.Settings], false, false); + } - // Filter out machine requests - const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); + // Filter out machine requests + const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); - assert.deepStrictEqual(actual, [ - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } } - ]); + assert.deepStrictEqual(actual, [ + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } } + ]); + }); }); test('test auto sync with non sync resource change triggers sync', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); - // Sync once and reset requests - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - target.reset(); + // Sync once and reset requests + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + target.reset(); - const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Trigger auto sync with window focus once - await testObject.triggerSync(['windowFocus'], true, false); + // Trigger auto sync with window focus once + await testObject.triggerSync(['windowFocus'], true, false); - // Filter out machine requests - const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); + // Filter out machine requests + const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); - // Make sure only one manifest request is made - assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + // Make sure only one manifest request is made + assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); }); test('test auto sync with non sync resource change does not trigger continuous syncs', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); - // Sync once and reset requests - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - target.reset(); + // Sync once and reset requests + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + target.reset(); - const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + const testObject: UserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Trigger auto sync with window focus multiple times - for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync(['windowFocus'], true, false); - } + // Trigger auto sync with window focus multiple times + for (let counter = 0; counter < 2; counter++) { + await testObject.triggerSync(['windowFocus'], true, false); + } - // Filter out machine requests - const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); + // Filter out machine requests + const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); - // Make sure only one manifest request is made - assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + // Make sure only one manifest request is made + assert.deepStrictEqual(actual, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); }); test('test first auto sync requests', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.sync(); - - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Machines - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, - // Settings - { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, - // Keybindings - { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, - // Snippets - { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, - // Tasks - { type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } }, - // Global state - { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, - // Extensions - { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, - // Machines - { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } } - ]); + await testObject.sync(); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, + // Settings + { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, + // Keybindings + { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, + // Tasks + { type: 'GET', url: `${target.url}/v1/resource/tasks/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/tasks`, headers: { 'If-Match': '0' } }, + // Global state + { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, + // Extensions + { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } } + ]); + }); }); test('test further auto sync requests without changes', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Sync once and reset requests - await testObject.sync(); - target.reset(); + // Sync once and reset requests + await testObject.sync(); + target.reset(); - await testObject.sync(); - - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } } - ]); + await testObject.sync(); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } } + ]); + }); }); test('test further auto sync requests with changes', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Sync once and reset requests - await testObject.sync(); - target.reset(); + // Sync once and reset requests + await testObject.sync(); + target.reset(); - // Do changes in the client - const fileService = client.instantiationService.get(IFileService); - const environmentService = client.instantiationService.get(IEnvironmentService); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); - await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); - await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); - await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); - await testObject.sync(); - - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, - // Settings - { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, - // Keybindings - { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, - // Snippets - { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, - // Global state - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, - ]); + // Do changes in the client + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); + await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); + await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + await testObject.sync(); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, + // Settings + { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, + // Keybindings + { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Snippets + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, + // Global state + { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, + ]); + }); }); test('test auto sync send execution id header', async () => { - // Setup the client - const target = new UserDataSyncTestServer(); - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); + await runWithFakedTimers({}, async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(client.instantiationService.createInstance(TestUserDataAutoSyncService)); - // Sync once and reset requests - await testObject.sync(); - target.reset(); + // Sync once and reset requests + await testObject.sync(); + target.reset(); - await testObject.sync(); + await testObject.sync(); - for (const request of target.requestsWithAllHeaders) { - const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0; - if (request.url.startsWith(`${target.url}/v1/resource/machines`)) { - assert.ok(!hasExecutionIdHeader, `Should not have execution header: ${request.url}`); - } else { - assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`); + for (const request of target.requestsWithAllHeaders) { + const hasExecutionIdHeader = request.headers && request.headers['X-Execution-Id'] && request.headers['X-Execution-Id'].length > 0; + if (request.url.startsWith(`${target.url}/v1/resource/machines`)) { + assert.ok(!hasExecutionIdHeader, `Should not have execution header: ${request.url}`); + } else { + assert.ok(hasExecutionIdHeader, `Should have execution header: ${request.url}`); + } } - } - + }); }); test('test delete on one client throws turned off error on other client while syncing', async () => { - const target = new UserDataSyncTestServer(); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(); - // Set up and sync from the client - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + // Set up and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.sync(); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + await testObject.sync(); - // Reset from the first client - await client.instantiationService.get(IUserDataSyncService).reset(); + // Reset from the first client + await client.instantiationService.get(IUserDataSyncService).reset(); - // Sync from the test client - target.reset(); + // Sync from the test client + target.reset(); - const errorPromise = Event.toPromise(testObject.onError); - await testObject.sync(); + const errorPromise = Event.toPromise(testObject.onError); + await testObject.sync(); - const e = await errorPromise; - assert.ok(e instanceof UserDataAutoSyncError); - assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TurnedOff); - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, - // Machine - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } }, - ]); + const e = await errorPromise; + assert.ok(e instanceof UserDataAutoSyncError); + assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TurnedOff); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, + // Machine + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } }, + ]); + }); }); test('test disabling the machine turns off sync', async () => { - const target = new UserDataSyncTestServer(); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.sync(); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + await testObject.sync(); - // Disable current machine - const userDataSyncMachinesService = testClient.instantiationService.get(IUserDataSyncMachinesService); - const machines = await userDataSyncMachinesService.getMachines(); - const currentMachine = machines.find(m => m.isCurrent)!; - await userDataSyncMachinesService.setEnablements([[currentMachine.id, false]]); + // Disable current machine + const userDataSyncMachinesService = testClient.instantiationService.get(IUserDataSyncMachinesService); + const machines = await userDataSyncMachinesService.getMachines(); + const currentMachine = machines.find(m => m.isCurrent)!; + await userDataSyncMachinesService.setEnablements([[currentMachine.id, false]]); - target.reset(); + target.reset(); - const errorPromise = Event.toPromise(testObject.onError); - await testObject.sync(); + const errorPromise = Event.toPromise(testObject.onError); + await testObject.sync(); - const e = await errorPromise; - assert.ok(e instanceof UserDataAutoSyncError); - assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TurnedOff); - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, - // Machine - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '2' } }, - { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } }, - ]); + const e = await errorPromise; + assert.ok(e instanceof UserDataAutoSyncError); + assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TurnedOff); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, + // Machine + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '2' } }, + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } }, + ]); + }); }); test('test removing the machine adds machine back', async () => { - const target = new UserDataSyncTestServer(); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.sync(); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + await testObject.sync(); - // Remove current machine - await testClient.instantiationService.get(IUserDataSyncMachinesService).removeCurrentMachine(); + // Remove current machine + await testClient.instantiationService.get(IUserDataSyncMachinesService).removeCurrentMachine(); - target.reset(); + target.reset(); - await testObject.sync(); - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, - // Machine - { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } }, - ]); + await testObject.sync(); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, + // Machine + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '2' } }, + ]); + }); }); test('test creating new session from one client throws session expired error on another client while syncing', async () => { - const target = new UserDataSyncTestServer(); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(); - // Set up and sync from the client - const client = disposableStore.add(new UserDataSyncClient(target)); - await client.setUp(); - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + // Set up and sync from the client + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.sync(); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + await testObject.sync(); - // Reset from the first client - await client.instantiationService.get(IUserDataSyncService).reset(); + // Reset from the first client + await client.instantiationService.get(IUserDataSyncService).reset(); - // Sync again from the first client to create new session - await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); + // Sync again from the first client to create new session + await (await client.instantiationService.get(IUserDataSyncService).createSyncTask(null)).run(); - // Sync from the test client - target.reset(); + // Sync from the test client + target.reset(); - const errorPromise = Event.toPromise(testObject.onError); - await testObject.sync(); + const errorPromise = Event.toPromise(testObject.onError); + await testObject.sync(); - const e = await errorPromise; - assert.ok(e instanceof UserDataAutoSyncError); - assert.deepStrictEqual((e).code, UserDataSyncErrorCode.SessionExpired); - assert.deepStrictEqual(target.requests, [ - // Manifest - { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, - // Machine - { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } }, - ]); + const e = await errorPromise; + assert.ok(e instanceof UserDataAutoSyncError); + assert.deepStrictEqual((e).code, UserDataSyncErrorCode.SessionExpired); + assert.deepStrictEqual(target.requests, [ + // Manifest + { type: 'GET', url: `${target.url}/v1/manifest`, headers: { 'If-None-Match': '1' } }, + // Machine + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } }, + ]); + }); }); test('test rate limit on server', async () => { - const target = new UserDataSyncTestServer(5); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(5); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - const errorPromise = Event.toPromise(testObject.onError); - while (target.requests.length < 5) { - await testObject.sync(); - } + const errorPromise = Event.toPromise(testObject.onError); + while (target.requests.length < 5) { + await testObject.sync(); + } - const e = await errorPromise; - assert.ok(e instanceof UserDataSyncStoreError); - assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TooManyRequests); + const e = await errorPromise; + assert.ok(e instanceof UserDataSyncStoreError); + assert.deepStrictEqual((e).code, UserDataSyncErrorCode.TooManyRequests); + }); }); test('test auto sync is suspended when server donot accepts requests', async () => { - const target = new UserDataSyncTestServer(5, 1); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(5, 1); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - while (target.requests.length < 5) { + while (target.requests.length < 5) { + await testObject.sync(); + } + + target.reset(); await testObject.sync(); - } - target.reset(); - await testObject.sync(); - - assert.deepStrictEqual(target.requests, []); + assert.deepStrictEqual(target.requests, []); + }); }); test('test cache control header with no cache is sent when triggered with disable cache option', async () => { - const target = new UserDataSyncTestServer(5, 1); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(5, 1); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.triggerSync(['some reason'], true, true); - assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); + await testObject.triggerSync(['some reason'], true, true); + assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); + }); }); test('test cache control header is not sent when triggered without disable cache option', async () => { - const target = new UserDataSyncTestServer(5, 1); + await runWithFakedTimers({}, async () => { + const target = new UserDataSyncTestServer(5, 1); - // Set up and sync from the test client - const testClient = disposableStore.add(new UserDataSyncClient(target)); - await testClient.setUp(); - const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = disposableStore.add(testClient.instantiationService.createInstance(TestUserDataAutoSyncService)); - await testObject.triggerSync(['some reason'], true, false); - assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); + await testObject.triggerSync(['some reason'], true, false); + assert.strictEqual(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); + }); }); }); diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index b8d59486d7a..d6a590c590e 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -98,6 +98,7 @@ export class WebClientServer { private readonly _staticRoute: string; private readonly _callbackRoute: string; + private readonly _webExtensionRoute: string; constructor( private readonly _connectionToken: ServerConnectionToken, @@ -110,6 +111,7 @@ export class WebClientServer { const serverRootPath = getRemoteServerRootPath(_productService); this._staticRoute = `${serverRootPath}/static`; this._callbackRoute = `${serverRootPath}/callback`; + this._webExtensionRoute = `${serverRootPath}/web-extension-resource`; } /** @@ -131,7 +133,7 @@ export class WebClientServer { // callback support return this._handleCallback(res); } - if (/^\/web-extension-resource\//.test(pathname)) { + if (pathname.startsWith(this._webExtensionRoute) && pathname.charCodeAt(this._webExtensionRoute.length) === CharCode.Slash) { // extension resource support return this._handleWebExtensionResource(req, res, parsedUrl); } @@ -177,7 +179,7 @@ export class WebClientServer { // Strip `/web-extension-resource/` from the path const normalizedPathname = decodeURIComponent(parsedUrl.pathname!); // support paths that are uri-encoded (e.g. spaces => %20) - const path = normalize(normalizedPathname.substr('/web-extension-resource/'.length)); + const path = normalize(normalizedPathname.substring(this._webExtensionRoute.length + 1)); const uri = URI.parse(path).with({ scheme: this._webExtensionResourceUrlTemplate.scheme, authority: path.substring(0, path.indexOf('/')), @@ -311,7 +313,7 @@ export class WebClientServer { 'resourceUrlTemplate': this._webExtensionResourceUrlTemplate.with({ scheme: 'http', authority: remoteAuthority, - path: `web-extension-resource/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` + path: `${this._webExtensionRoute}/${this._webExtensionResourceUrlTemplate.authority}${this._webExtensionResourceUrlTemplate.path}` }).toString(true) } : undefined }, diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index e01c59d4ec4..16fada27b5b 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -30,7 +30,7 @@ import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape, reviveWorkspaceEditDto } from '../common/extHost.protocol'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; @extHostNamedCustomer(MainContext.MainThreadLanguageFeatures) export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape { @@ -863,19 +863,19 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- document drop Edits - private readonly _documentOnDropProviders = new Map(); + private readonly _documentOnDropEditProviders = new Map(); - $registerDocumentOnDropProvider(handle: number, selector: IDocumentFilterDto[]): void { - const provider = new MainThreadDocumentOnDropProvider(handle, this._proxy); - this._documentOnDropProviders.set(handle, provider); + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[]): void { + const provider = new MainThreadDocumentOnDropEditProvider(handle, this._proxy); + this._documentOnDropEditProviders.set(handle, provider); this._registrations.set(handle, combinedDisposable( this._languageFeaturesService.documentOnDropEditProvider.register(selector, provider), - toDisposable(() => this._documentOnDropProviders.delete(handle)), + toDisposable(() => this._documentOnDropEditProviders.delete(handle)), )); } async $resolveDocumentOnDropFileData(handle: number, requestId: number, dataIndex: number): Promise { - const provider = this._documentOnDropProviders.get(handle); + const provider = this._documentOnDropEditProviders.get(handle); if (!provider) { throw new Error('Could not find provider'); } @@ -883,7 +883,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread } } -class MainThreadDocumentOnDropProvider implements languages.DocumentOnDropEditProvider { +class MainThreadDocumentOnDropEditProvider implements languages.DocumentOnDropEditProvider { private readonly dataTransfers = new DataTransferCache(); diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 54e79e9918c..6e96ff8161f 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -15,7 +15,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ILogService } from 'vs/platform/log/common/log'; import { DataTransferConverter } from 'vs/workbench/api/common/shared/dataTransfer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; import { VSBuffer } from 'vs/base/common/buffer'; import { DataTransferCache } from 'vs/workbench/api/common/shared/dataTransferCache'; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d7da9d1ee99..313a9aadfd1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -453,8 +453,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostLanguages.changeLanguage(document.uri, languageId); }, match(selector: vscode.DocumentSelector, document: vscode.TextDocument): number { - const notebookType = extHostDocuments.getDocumentData(document.uri)?.notebook?.notebookType; - return score(typeConverters.LanguageSelector.from(selector), document.uri, document.languageId, true, notebookType); + const notebook = extHostDocuments.getDocumentData(document.uri)?.notebook; + return score(typeConverters.LanguageSelector.from(selector), document.uri, document.languageId, true, notebook?.uri, notebook?.notebookType); }, registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider, metadata?: vscode.CodeActionProviderMetadata): vscode.Disposable { return extHostLanguageFeatures.registerCodeActionProvider(extension, checkSelector(selector), provider, metadata); @@ -570,9 +570,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createLanguageStatusItem(id: string, selector: vscode.DocumentSelector): vscode.LanguageStatusItem { return extHostLanguages.createLanguageStatusItem(extension, id, selector); }, - registerDocumentOnDropProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropProvider): vscode.Disposable { + registerDocumentOnDropEditProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropEditProvider): vscode.Disposable { checkProposedApiEnabled(extension, 'textEditorDrop'); - return extHostLanguageFeatures.registerDocumentOnDropProvider(extension, selector, provider); + return extHostLanguageFeatures.registerDocumentOnDropEditProvider(extension, selector, provider); } }; @@ -1321,6 +1321,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookCellOutputItem: extHostTypes.NotebookCellOutputItem, NotebookCellStatusBarItem: extHostTypes.NotebookCellStatusBarItem, NotebookControllerAffinity: extHostTypes.NotebookControllerAffinity, + NotebookEdit: extHostTypes.NotebookEdit, PortAttributes: extHostTypes.PortAttributes, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestResultState: extHostTypes.TestResultState, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9fa08b4b6c3..6d936351c35 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -392,7 +392,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerTypeHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void; - $registerDocumentOnDropProvider(handle: number, selector: IDocumentFilterDto[]): void; + $registerDocumentOnDropEditProvider(handle: number, selector: IDocumentFilterDto[]): void; $resolveDocumentOnDropFileData(handle: number, requestId: number, dataIndex: number): Promise; $setLanguageConfiguration(handle: number, languageId: string, configuration: ILanguageConfigurationDto): void; } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 32bf1eab953..3b6b789641a 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1745,12 +1745,12 @@ class TypeHierarchyAdapter { } } -class DocumentOnDropAdapter { +class DocumentOnDropEditAdapter { constructor( private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape, private readonly _documents: ExtHostDocuments, - private readonly _provider: vscode.DocumentOnDropProvider, + private readonly _provider: vscode.DocumentOnDropEditProvider, private readonly _handle: number, ) { } @@ -1778,7 +1778,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter | LinkedEditingRangeAdapter | InlayHintsAdapter | InlineCompletionAdapter | InlineCompletionAdapterNew - | DocumentOnDropAdapter; + | DocumentOnDropEditAdapter; class AdapterData { constructor( @@ -2402,15 +2402,15 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- Document on drop - registerDocumentOnDropProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropProvider) { + registerDocumentOnDropEditProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentOnDropEditProvider) { const handle = this._nextHandle(); - this._adapter.set(handle, new AdapterData(new DocumentOnDropAdapter(this._proxy, this._documents, provider, handle), extension)); - this._proxy.$registerDocumentOnDropProvider(handle, this._transformDocumentSelector(selector)); + this._adapter.set(handle, new AdapterData(new DocumentOnDropEditAdapter(this._proxy, this._documents, provider, handle), extension)); + this._proxy.$registerDocumentOnDropEditProvider(handle, this._transformDocumentSelector(selector)); return this._createDisposable(handle); } $provideDocumentOnDropEdits(handle: number, requestId: number, resource: UriComponents, position: IPosition, dataTransferDto: DataTransferDTO, token: CancellationToken): Promise | undefined> { - return this._withAdapter(handle, DocumentOnDropAdapter, adapter => + return this._withAdapter(handle, DocumentOnDropEditAdapter, adapter => Promise.resolve(adapter.provideDocumentOnDropEdits(requestId, URI.revive(resource), position, dataTransferDto, token)), undefined, undefined); } diff --git a/src/vs/workbench/api/common/extHostNotebookEditor.ts b/src/vs/workbench/api/common/extHostNotebookEditor.ts index 3b1a53197b3..b18e816f181 100644 --- a/src/vs/workbench/api/common/extHostNotebookEditor.ts +++ b/src/vs/workbench/api/common/extHostNotebookEditor.ts @@ -105,6 +105,15 @@ export class ExtHostNotebookEditor { get document() { return that.notebookData.apiNotebook; }, + get notebook() { + return that.notebookData.apiNotebook; + }, + get selection() { + return that._selections[0]; + }, + set selection(selection: vscode.NotebookRange) { + this.selections = [selection]; + }, get selections() { return that._selections; }, diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 960b8d39a9e..b984ae6569e 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -25,7 +25,7 @@ import { Command } from 'vs/editor/common/languages'; import { DataTransferConverter, DataTransferDTO } from 'vs/workbench/api/common/shared/dataTransfer'; import { ITreeViewsService, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; type TreeItemHandle = string; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 977f0cdf142..dc08ede3d98 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -598,6 +598,55 @@ export class TextEdit { } } +@es5ClassCompat +export class NotebookEdit implements vscode.NotebookEdit { + + static isNotebookCellEdit(thing: any): thing is NotebookEdit { + if (thing instanceof NotebookEdit) { + return true; + } + if (!thing) { + return false; + } + return NotebookRange.isNotebookRange((thing)) + && Array.isArray((thing).newCells); + } + + static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit { + return new NotebookEdit(range, newCells); + } + + static insertCells(index: number, newCells: vscode.NotebookCellData[]): vscode.NotebookEdit { + return new NotebookEdit(new NotebookRange(index, index), newCells); + } + + static deleteCells(range: NotebookRange): NotebookEdit { + return new NotebookEdit(range, []); + } + + static updateCellMetadata(index: number, newMetadata: { [key: string]: any }): NotebookEdit { + const edit = new NotebookEdit(new NotebookRange(index, index), []); + edit.newCellMetadata = newMetadata; + return edit; + } + + static updateNotebookMetadata(newMetadata: { [key: string]: any }): NotebookEdit { + const edit = new NotebookEdit(new NotebookRange(0, 0), []); + edit.newNotebookMetadata = newMetadata; + return edit; + } + + range: NotebookRange; + newCells: NotebookCellData[]; + newCellMetadata?: { [key: string]: any }; + newNotebookMetadata?: { [key: string]: any }; + + constructor(range: NotebookRange, newCells: NotebookCellData[]) { + this.range = range; + this.newCells = newCells; + } +} + export class SnippetTextEdit implements vscode.SnippetTextEdit { range: vscode.Range; @@ -741,7 +790,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); } - set(uri: URI, edits: TextEdit[]): void { + set(uri: URI, edits: TextEdit[] | unknown): void { if (!edits) { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { @@ -753,9 +802,17 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { coalesceInPlace(this._edits); } else { // append edit to the end - for (const edit of edits) { + for (const edit of edits as TextEdit[] | NotebookEdit[]) { if (edit) { - this._edits.push({ _type: FileEditType.Text, uri, edit }); + if (NotebookEdit.isNotebookCellEdit(edit)) { + if (edit.newCellMetadata) { + this.replaceNotebookCellMetadata(uri, edit.range.start, edit.newCellMetadata); + } else { + this.replaceNotebookCells(uri, edit.range, edit.newCells); + } + } else { + this._edits.push({ _type: FileEditType.Text, uri, edit }); + } } } } diff --git a/src/vs/workbench/api/common/shared/dataTransfer.ts b/src/vs/workbench/api/common/shared/dataTransfer.ts index dd4749b607b..a16dfe65f96 100644 --- a/src/vs/workbench/api/common/shared/dataTransfer.ts +++ b/src/vs/workbench/api/common/shared/dataTransfer.ts @@ -5,7 +5,7 @@ import { once } from 'vs/base/common/functional'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IDataTransfer, IDataTransferItem } from 'vs/editor/common/dnd'; +import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; export interface IDataTransferFileDTO { readonly name: string; diff --git a/src/vs/workbench/api/common/shared/dataTransferCache.ts b/src/vs/workbench/api/common/shared/dataTransferCache.ts index c3e845c61d4..02184d68d18 100644 --- a/src/vs/workbench/api/common/shared/dataTransferCache.ts +++ b/src/vs/workbench/api/common/shared/dataTransferCache.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; -import { IDataTransfer, IDataTransferItem } from 'vs/editor/common/dnd'; +import { IDataTransfer, IDataTransferItem } from 'vs/base/common/dataTransfer'; export class DataTransferCache { diff --git a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts index a3cada12b00..a457ca659ca 100644 --- a/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDocumentData.test.ts @@ -12,6 +12,7 @@ import { MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protoc import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import { mock } from 'vs/base/test/common/mock'; import * as perfData from './extHostDocumentData.test.perf-data'; +import { setDefaultGetWordAtTextConfig } from 'vs/editor/common/core/wordHelper'; suite('ExtHostDocumentData', () => { @@ -316,14 +317,22 @@ suite('ExtHostDocumentData', () => { perfData._$_$_expensive ], '\n', 1, 'text', false); - let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!; - assert.strictEqual(range, undefined); + // this test only ensures that we eventually give and timeout (when searching "funny" words and long lines) + // for the sake of speedy tests we lower the timeBudget here + const config = setDefaultGetWordAtTextConfig({ maxLen: 1000, windowSize: 15, timeBudget: 30 }); + try { + let range = data.document.getWordRangeAtPosition(new Position(0, 1_177_170), regex)!; + assert.strictEqual(range, undefined); - const pos = new Position(0, 1177170); - range = data.document.getWordRangeAtPosition(pos)!; - assert.ok(range); - assert.ok(range.contains(pos)); - assert.strictEqual(data.document.getText(range), 'TaskDefinition'); + const pos = new Position(0, 1177170); + range = data.document.getWordRangeAtPosition(pos)!; + assert.ok(range); + assert.ok(range.contains(pos)); + assert.strictEqual(data.document.getText(range), 'TaskDefinition'); + + } finally { + config.dispose(); + } }); test('Rename popup sometimes populates with text on the left side omitted #96013', function () { diff --git a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts index 058a03faa02..48e90584747 100644 --- a/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTreeViews.test.ts @@ -19,6 +19,7 @@ import { TreeItemCollapsibleState, ITreeItem, IRevealOptions } from 'vs/workbenc import { NullLogService } from 'vs/platform/log/common/log'; import type { IDisposable } from 'vs/base/common/lifecycle'; import { nullExtensionDescription as extensionsDescription } from 'vs/workbench/services/extensions/common/extensions'; +import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; suite('ExtHostTreeView', function () { @@ -250,15 +251,17 @@ suite('ExtHostTreeView', function () { }); async function runWithEventMerging(action: (resolve: () => void) => void) { - await new Promise((resolve) => { - let subscription: IDisposable | undefined = undefined; - subscription = target.onRefresh.event(() => { - subscription!.dispose(); - resolve(); + await runWithFakedTimers({}, async () => { + await new Promise((resolve) => { + let subscription: IDisposable | undefined = undefined; + subscription = target.onRefresh.event(() => { + subscription!.dispose(); + resolve(); + }); + onDidChangeTreeNode.fire(getNode('b')); }); - onDidChangeTreeNode.fire(getNode('b')); + await new Promise(action); }); - await new Promise(action); } test('refresh parent and child node trigger refresh only on parent - scenario 1', async () => { diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 1eff30cf896..8c08c8e8057 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -140,14 +140,10 @@ registerAction2(class QuickAccessAction extends Action2 { mac: globalQuickAccessKeybinding.mac }, f1: true, - menu: [{ - id: MenuId.TitleMenuQuickPick, - group: '1/workspaceNav', - order: 1 - }, { + menu: { id: MenuId.TitleMenu, order: 100 - }] + } }); } diff --git a/src/vs/workbench/browser/codeeditor.ts b/src/vs/workbench/browser/codeeditor.ts index bc251d1bda1..0b23958dfed 100644 --- a/src/vs/workbench/browser/codeeditor.ts +++ b/src/vs/workbench/browser/codeeditor.ts @@ -47,8 +47,11 @@ export class RangeHighlightDecorations extends Disposable { } removeHighlightRange() { - if (this.editor?.getModel() && this.rangeHighlightDecorationId) { - this.editor.deltaDecorations([this.rangeHighlightDecorationId], []); + if (this.editor && this.rangeHighlightDecorationId) { + const decorationId = this.rangeHighlightDecorationId; + this.editor.changeDecorations((accessor) => { + accessor.removeDecoration(decorationId); + }); this._onHighlightRemoved.fire(); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index b1f9001e03c..63cfe74d6de 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -3,45 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; -import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { VSBuffer } from 'vs/base/common/buffer'; -import Severity from 'vs/base/common/severity'; -import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { basename, isEqual } from 'vs/base/common/resources'; -import { ByteSize, IFileService } from 'vs/platform/files/common/files'; -import { IWindowOpenable } from 'vs/platform/window/common/window'; -import { URI } from 'vs/base/common/uri'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IBaseTextResourceEditorInput } from 'vs/platform/editor/common/editor'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; -import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Mimes } from 'vs/base/common/mime'; -import { isWeb, isWindows } from 'vs/base/common/platform'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorIdentifier, GroupIdentifier, isEditorIdentifier, EditorResourceAccessor } from 'vs/workbench/common/editor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { addDisposableListener, DragAndDropObserver, EventType } from 'vs/base/browser/dom'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { Emitter } from 'vs/base/common/event'; -import { coalesce } from 'vs/base/common/arrays'; -import { parse, stringify } from 'vs/base/common/marshalling'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { hasWorkspaceFileExtension, isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { withNullAsUndefined } from 'vs/base/common/types'; -import { IDataTransfer } from 'vs/editor/common/dnd'; -import { extractSelection } from 'vs/platform/opener/common/opener'; +import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; import { IListDragAndDrop } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; -import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; -import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; -import { DeferredPromise } from 'vs/base/common/async'; +import { coalesce } from 'vs/base/common/arrays'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { stringify } from 'vs/base/common/marshalling'; +import { Mimes } from 'vs/base/common/mime'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { isWindows } from 'vs/base/common/platform'; +import { basename, isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { CodeDataTransfers, createDraggedEditorInputFromRawResourcesData, Extensions, extractEditorsDropData, IDragAndDropContributionRegistry, IDraggedResourceEditorInput, IResourceStat } from 'vs/platform/dnd/browser/dnd'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { extractSelection } from 'vs/platform/opener/common/opener'; import { Registry } from 'vs/platform/registry/common/platform'; +import { IWindowOpenable } from 'vs/platform/window/common/window'; +import { hasWorkspaceFileExtension, isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceFolderCreationData, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { EditorResourceAccessor, GroupIdentifier, IEditorIdentifier, isEditorIdentifier } from 'vs/workbench/common/editor'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; //#region Editor / Resources DND @@ -60,124 +52,6 @@ export class DraggedTreeItemsIdentifier { constructor(readonly identifier: string) { } } -export const CodeDataTransfers = { - EDITORS: 'CodeEditors', - FILES: 'CodeFiles' -}; - -export interface IDraggedResourceEditorInput extends IBaseTextResourceEditorInput { - resource: URI | undefined; - - /** - * A hint that the source of the dragged editor input - * might not be the application but some external tool. - */ - isExternal?: boolean; - - /** - * Whether we probe for the dropped editor to be a workspace - * (i.e. code-workspace file or even a folder), allowing to - * open it as workspace instead of opening as editor. - */ - allowWorkspaceOpen?: boolean; -} - -export async function extractEditorsDropData(accessor: ServicesAccessor, e: DragEvent): Promise> { - const editors: IDraggedResourceEditorInput[] = []; - if (e.dataTransfer && e.dataTransfer.types.length > 0) { - - // Data Transfer: Code Editors - const rawEditorsData = e.dataTransfer.getData(CodeDataTransfers.EDITORS); - if (rawEditorsData) { - try { - editors.push(...parse(rawEditorsData)); - } catch (error) { - // Invalid transfer - } - } - - // Data Transfer: Resources - else { - try { - const rawResourcesData = e.dataTransfer.getData(DataTransfers.RESOURCES); - editors.push(...createDraggedEditorInputFromRawResourcesData(rawResourcesData)); - } catch (error) { - // Invalid transfer - } - } - - // Check for native file transfer - if (e.dataTransfer?.files) { - for (let i = 0; i < e.dataTransfer.files.length; i++) { - const file = e.dataTransfer.files[i]; - if (file?.path /* Electron only */) { - try { - editors.push({ resource: URI.file(file.path), isExternal: true, allowWorkspaceOpen: true }); - } catch (error) { - // Invalid URI - } - } - } - } - - // Check for CodeFiles transfer - const rawCodeFiles = e.dataTransfer.getData(CodeDataTransfers.FILES); - if (rawCodeFiles) { - try { - const codeFiles: string[] = JSON.parse(rawCodeFiles); - for (const codeFile of codeFiles) { - editors.push({ resource: URI.file(codeFile), isExternal: true, allowWorkspaceOpen: true }); - } - } catch (error) { - // Invalid transfer - } - } - - // Web: Check for file transfer - if (isWeb && containsDragType(e, DataTransfers.FILES)) { - const files = e.dataTransfer.items; - if (files) { - const instantiationService = accessor.get(IInstantiationService); - const filesData = await instantiationService.invokeFunction(accessor => extractFilesDropData(accessor, e)); - for (const fileData of filesData) { - editors.push({ resource: fileData.resource, contents: fileData.contents?.toString(), isExternal: true, allowWorkspaceOpen: fileData.isDirectory }); - } - } - } - - // Workbench contributions - const contributions = Registry.as(Extensions.DragAndDropContribution).getAll(); - for (const contribution of contributions) { - const data = e.dataTransfer.getData(contribution.dataFormatKey); - if (data) { - try { - editors.push(...contribution.getEditorInputs(data)); - } catch (error) { - // Invalid transfer - } - } - } - } - - return editors; -} - -function createDraggedEditorInputFromRawResourcesData(rawResourcesData: string | undefined): IDraggedResourceEditorInput[] { - const editors: IDraggedResourceEditorInput[] = []; - - if (rawResourcesData) { - const resourcesRaw: string[] = JSON.parse(rawResourcesData); - for (const resourceRaw of resourcesRaw) { - if (resourceRaw.indexOf(':') > 0) { // mitigate https://github.com/microsoft/vscode/issues/124946 - const { selection, uri } = extractSelection(URI.parse(resourceRaw)); - editors.push({ resource: uri, options: { selection } }); - } - } - } - - return editors; -} - export async function extractTreeDropData(dataTransfer: IDataTransfer): Promise> { const editors: IDraggedResourceEditorInput[] = []; const resourcesKey = Mimes.uriList.toLowerCase(); @@ -201,121 +75,6 @@ export function convertResourceUrlsToUriList(resourceUrls: string): string { return asJson.map(uri => uri.toString()).join('\n'); } -interface IFileTransferData { - resource: URI; - isDirectory?: boolean; - contents?: VSBuffer; -} - -async function extractFilesDropData(accessor: ServicesAccessor, event: DragEvent): Promise { - - // Try to extract via `FileSystemHandle` - if (WebFileSystemAccess.supported(window)) { - const items = event.dataTransfer?.items; - if (items) { - return extractFileTransferData(accessor, items); - } - } - - // Try to extract via `FileList` - const files = event.dataTransfer?.files; - if (!files) { - return []; - } - - return extractFileListData(accessor, files); -} - -async function extractFileTransferData(accessor: ServicesAccessor, items: DataTransferItemList): Promise { - const fileSystemProvider = accessor.get(IFileService).getProvider(Schemas.file); - if (!(fileSystemProvider instanceof HTMLFileSystemProvider)) { - return []; // only supported when running in web - } - - const results: DeferredPromise[] = []; - - for (let i = 0; i < items.length; i++) { - const file = items[i]; - if (file) { - const result = new DeferredPromise(); - results.push(result); - - (async () => { - try { - const handle = await file.getAsFileSystemHandle(); - if (!handle) { - result.complete(undefined); - return; - } - - if (WebFileSystemAccess.isFileSystemFileHandle(handle)) { - result.complete({ - resource: await fileSystemProvider.registerFileHandle(handle), - isDirectory: false - }); - } else if (WebFileSystemAccess.isFileSystemDirectoryHandle(handle)) { - result.complete({ - resource: await fileSystemProvider.registerDirectoryHandle(handle), - isDirectory: true - }); - } else { - result.complete(undefined); - } - } catch (error) { - result.complete(undefined); - } - })(); - } - } - - return coalesce(await Promise.all(results.map(result => result.p))); -} - -export async function extractFileListData(accessor: ServicesAccessor, files: FileList): Promise { - const dialogService = accessor.get(IDialogService); - - const results: DeferredPromise[] = []; - - for (let i = 0; i < files.length; i++) { - const file = files.item(i); - if (file) { - - // Skip for very large files because this operation is unbuffered - if (file.size > 100 * ByteSize.MB) { - dialogService.show(Severity.Warning, localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); - continue; - } - - const result = new DeferredPromise(); - results.push(result); - - const reader = new FileReader(); - - reader.onerror = () => result.complete(undefined); - reader.onabort = () => result.complete(undefined); - - reader.onload = async event => { - const name = file.name; - const loadResult = withNullAsUndefined(event.target?.result); - if (typeof name !== 'string' || typeof loadResult === 'undefined') { - result.complete(undefined); - return; - } - - result.complete({ - resource: URI.from({ scheme: Schemas.untitled, path: name }), - contents: typeof loadResult === 'string' ? VSBuffer.fromString(loadResult) : VSBuffer.wrap(new Uint8Array(loadResult)) - }); - }; - - // Start reading - reader.readAsArrayBuffer(file); - } - } - - return coalesce(await Promise.all(results.map(result => result.p))); -} - export interface IResourcesDropHandlerOptions { /** @@ -443,11 +202,6 @@ export class ResourcesDropHandler { } } -interface IResourceStat { - resource: URI; - isDirectory?: boolean; -} - export function fillEditorsDragData(accessor: ServicesAccessor, resources: URI[], event: DragMouseEvent | DragEvent): void; export function fillEditorsDragData(accessor: ServicesAccessor, resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; export function fillEditorsDragData(accessor: ServicesAccessor, editors: IEditorIdentifier[], event: DragMouseEvent | DragEvent): void; @@ -591,48 +345,6 @@ export function fillEditorsDragData(accessor: ServicesAccessor, resourcesOrEdito //#endregion -//#region DND contributions - -export interface IDragAndDropContributionRegistry { - /** - * Registers a drag and drop contribution. - */ - register(contribution: IDragAndDropContribution): void; - - /** - * Returns all registered drag and drop contributions. - */ - getAll(): IterableIterator; -} - -export interface IDragAndDropContribution { - readonly dataFormatKey: string; - getEditorInputs(data: string): IDraggedResourceEditorInput[]; - setData(resources: IResourceStat[], event: DragMouseEvent | DragEvent): void; -} - -class DragAndDropContributionRegistry implements IDragAndDropContributionRegistry { - private readonly _contributions = new Map(); - - register(contribution: IDragAndDropContribution): void { - if (this._contributions.has(contribution.dataFormatKey)) { - throw new Error(`A drag and drop contributiont with key '${contribution.dataFormatKey}' was already registered.`); - } - this._contributions.set(contribution.dataFormatKey, contribution); - } - - getAll(): IterableIterator { - return this._contributions.values(); - } -} - -export const Extensions = { - DragAndDropContribution: 'workbench.contributions.dragAndDrop' -}; - -Registry.add(Extensions.DragAndDropContribution, new DragAndDropContributionRegistry()); - -//#endregion //#region DND Utilities @@ -681,26 +393,6 @@ export class LocalSelectionTransfer { } } -export function containsDragType(event: DragEvent, ...dragTypesToFind: string[]): boolean { - if (!event.dataTransfer) { - return false; - } - - const dragTypes = event.dataTransfer.types; - const lowercaseDragTypes: string[] = []; - for (let i = 0; i < dragTypes.length; i++) { - lowercaseDragTypes.push(dragTypes[i].toLowerCase()); // somehow the types are lowercase - } - - for (const dragType of dragTypesToFind) { - if (lowercaseDragTypes.indexOf(dragType.toLowerCase()) >= 0) { - return true; - } - } - - return false; -} - //#endregion //#region Composites DND diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 0f2382f01a4..3d61bbcb255 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -135,50 +135,6 @@ body.web { -webkit-app-region: no-drag; } -.monaco-workbench .monaco-menu .monaco-action-bar.vertical { - padding: .5em 0; -} - -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .action-menu-item { - height: 1.8em; -} - -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .keybinding { - font-size: inherit; - padding: 0 2em; -} - -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .menu-item-check { - font-size: inherit; - width: 2em; -} - -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .action-label.separator { - font-size: inherit; - padding: 0.2em 0 0 0; - margin-bottom: 0.2em; -} - -.monaco-workbench.linux .monaco-menu .monaco-action-bar.vertical .action-label.separator { - margin-left: 0; - margin-right: 0; -} - -.monaco-workbench .monaco-menu .monaco-action-bar.vertical .submenu-indicator { - font-size: 60%; - padding: 0 1.8em; -} - -.monaco-workbench.linux .monaco-menu .monaco-action-bar.vertical .submenu-indicator { - height: 100%; - mask-size: 10px 10px; - -webkit-mask-size: 10px 10px; -} - -.monaco-workbench .monaco-menu .action-item { - cursor: default; -} .monaco-workbench .codicon[class*='codicon-'] { font-size: 16px; /* sets font-size for codicons in workbench (https://github.com/microsoft/vscode/issues/98495) */ diff --git a/src/vs/workbench/browser/parts/banner/media/bannerpart.css b/src/vs/workbench/browser/parts/banner/media/bannerpart.css index fb2d5b4d1e8..aa793f995a6 100644 --- a/src/vs/workbench/browser/parts/banner/media/bannerpart.css +++ b/src/vs/workbench/browser/parts/banner/media/bannerpart.css @@ -52,10 +52,12 @@ padding: 3px; margin-left: 12px; text-decoration: underline; + cursor: pointer; } .monaco-workbench .part.banner .message-container a { text-decoration: underline; + cursor: pointer; } .monaco-workbench .part.banner .action-container { diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index f81d3790b5d..ad6a0289f12 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -18,7 +18,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { CodeDataTransfers, containsDragType, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, DraggedTreeItemsIdentifier, Extensions as DragAndDropExtensions, extractTreeDropData, IDragAndDropContributionRegistry, LocalSelectionTransfer, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType, Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry } from 'vs/platform/dnd/browser/dnd'; +import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, DraggedTreeItemsIdentifier, extractTreeDropData, LocalSelectionTransfer, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; import { fillActiveEditorViewState, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; diff --git a/src/vs/workbench/browser/parts/panel/media/basepanelpart.css b/src/vs/workbench/browser/parts/panel/media/basepanelpart.css index 6cb382b8a5d..1f4f10f21c3 100644 --- a/src/vs/workbench/browser/parts/panel/media/basepanelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/basepanelpart.css @@ -176,13 +176,13 @@ } .monaco-workbench .part.basepanel > .composite.title > .panel-switcher-container > .monaco-action-bar .action-item .active-item-indicator { - top: -6px; + top: -4px; left: 10px; width: calc(100% - 20px); } .monaco-workbench .part.basepanel > .composite.title > .panel-switcher-container > .monaco-action-bar .action-item.icon .active-item-indicator { - top: -1px; + top: 1px; left: 2px; width: calc(100% - 4px); } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 5ffa3a07bbc..453dd8c5f11 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -378,7 +378,7 @@ export abstract class BasePanelPart extends CompositePart impleme contextKey.set(true); this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); - if (this.layoutService.isVisible(this.partId)) { + if (this.layoutService.isRestored() && this.layoutService.isVisible(this.partId)) { const activeComposite = this.getActiveComposite(); if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { this.compositeBar.activateComposite(viewContainer.id); diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index 69725b8cd64..35b374ece47 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -83,6 +83,7 @@ .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu { z-index: 3000; -webkit-app-region: no-drag; + padding: 0 8px; } .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu.hide { @@ -92,6 +93,7 @@ .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen { display: flex; color: var(--vscode-titleMenu-foreground); + background-color: var(--vscode-titleMenu-background); border: 1px solid var(--vscode-titleMenu-border); border-radius: 5px; height: 20px; @@ -105,19 +107,48 @@ } .monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen:HOVER { + color: var(--vscode-titleMenu-activeForeground); background-color: var(--vscode-titleMenu-activeBackground); line-height: 18px; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen .action-label { - display: inline-block; - width: 100%; - text-align: center; - font-size: 12px; +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu:HOVER .quickopen .action-label { + background-color: transparent !important; + outline-color: transparent !important; } -.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen .action-label:HOVER { - background-color: var(--vscode-titleMenu-activeBackground); +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search { + display: inline-flex; + text-align: center; + font-size: 12px; + line-height: 14px; + background-color: inherit; + justify-content: center; + width: calc(100% - 19px); +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search>.search-icon { + font-size: 14px; + opacity: .8; + padding: 0 3px; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.action-label.search>.search-label { + overflow: hidden; + text-overflow: ellipsis; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu .action-item.quickopen>.all-options>.action-label { + text-align: center; + font-size: 12px; + width: 16px; + border-left: 1px solid transparent; + border-radius: 0; + padding-right: 0; +} + +.monaco-workbench .part.titlebar>.titlebar-container>.window-title>.title-menu:HOVER .action-item.quickopen>.all-options>.action-label { + border-color: var(--vscode-titleMenu-border); } /* Menubar */ diff --git a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts b/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts index f662aa9b832..a68616a47d8 100644 --- a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts @@ -3,25 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { reset } from 'vs/base/browser/dom'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IAction } from 'vs/base/common/actions'; +import { Action, IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; +import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, MenuId, MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import * as colors from 'vs/platform/theme/common/colorRegistry'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; -import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND } from 'vs/workbench/common/theme'; +import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TitleMenuControl { private readonly _disposables = new DisposableStore(); + private readonly _onDidChangeVisibility = new Emitter(); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + readonly element: HTMLElement = document.createElement('div'); constructor( @@ -31,31 +43,83 @@ export class TitleMenuControl { @IInstantiationService instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, ) { this.element.classList.add('title-menu'); + + const hoverDelegate = new class implements IHoverDelegate { + + private _lastHoverHideTime: number = 0; + + readonly showHover = hoverService.showHover.bind(hoverService); + readonly placement = 'element'; + + get delay(): number { + return Date.now() - this._lastHoverHideTime < 200 + ? 0 // show instantly when a hover was recently shown + : configurationService.getValue('workbench.hover.delay'); + } + + onDidHideHover() { + this._lastHoverHideTime = Date.now(); + } + }; + const titleToolbar = new ToolBar(this.element, contextMenuService, { actionViewItemProvider: (action) => { if (action instanceof MenuItemAction && action.id === 'workbench.action.quickOpen') { class InputLikeViewItem extends MenuEntryActionViewItem { + + private readonly workspaceTitle = document.createElement('span'); + override render(container: HTMLElement): void { super.render(container); container.classList.add('quickopen'); - this._store.add(windowTitle.onDidChange(this._updateFromWindowTitle, this)); + + assertType(this.label); + this.label.classList.add('search'); + + const searchIcon = renderIcon(Codicon.search); + searchIcon.classList.add('search-icon'); + + this.workspaceTitle.classList.add('search-label'); this._updateFromWindowTitle(); + reset(this.label, searchIcon, this.workspaceTitle); + this._renderAllQuickPickItem(container); + + this._store.add(windowTitle.onDidChange(this._updateFromWindowTitle, this)); } + private _updateFromWindowTitle() { - if (this.label) { - this.label.innerText = localize('search', "Search {0}", windowTitle.workspaceName); - this.label.title = windowTitle.value; - } + this.workspaceTitle.innerText = windowTitle.workspaceName; + const kb = keybindingService.lookupKeybinding(action.id)?.getLabel(); + const title = kb + ? localize('title', "Search {0} ({1}) \u2014 {2}", windowTitle.workspaceName, kb, windowTitle.value) + : localize('title2', "Search {0} \u2014 {1}", windowTitle.workspaceName, windowTitle.value); + this._applyUpdateTooltip(title); + } + + private _renderAllQuickPickItem(parent: HTMLElement): void { + const container = document.createElement('span'); + container.classList.add('all-options'); + parent.appendChild(container); + const action = new Action('all', localize('all', "Show Quick Pick Options..."), Codicon.chevronDown.classNames, true, () => { + quickInputService.quickAccess.show('?'); + }); + const dropdown = new ActionViewItem(undefined, action, { icon: true, label: false, hoverDelegate }); + dropdown.render(container); + this._store.add(dropdown); + this._store.add(action); } } - return instantiationService.createInstance(InputLikeViewItem, action, undefined); + return instantiationService.createInstance(InputLikeViewItem, action, { hoverDelegate }); } - return createActionViewItem(instantiationService, action); + return createActionViewItem(instantiationService, action, { hoverDelegate }); } }); const titleMenu = this._disposables.add(menuService.createMenu(MenuId.TitleMenu, contextKeyService)); @@ -68,8 +132,13 @@ export class TitleMenuControl { }; updateTitleMenu(); this._disposables.add(titleMenu.onDidChange(updateTitleMenu)); - this._disposables.add(quickInputService.onShow(() => this.element.classList.toggle('hide', true))); - this._disposables.add(quickInputService.onHide(() => this.element.classList.toggle('hide', false))); + this._disposables.add(quickInputService.onShow(this._setVisibility.bind(this, false))); + this._disposables.add(quickInputService.onHide(this._setVisibility.bind(this, true))); + } + + private _setVisibility(show: boolean): void { + this.element.classList.toggle('hide', !show); + this._onDidChangeVisibility.fire(); } dispose(): void { @@ -77,27 +146,25 @@ export class TitleMenuControl { } } -// --- quick pick submenu - -MenuRegistry.appendMenuItem(MenuId.TitleMenu, { - submenu: MenuId.TitleMenuQuickPick, - title: localize('title', "Quick Pick"), - icon: Codicon.search, - order: Number.MAX_SAFE_INTEGER -}); - - // --- theme colors +// foreground (inactive and active) colors.registerColor( 'titleMenu.foreground', - { dark: colors.inputForeground, hcDark: colors.inputForeground, light: colors.inputForeground, hcLight: colors.inputForeground }, + { dark: TITLE_BAR_ACTIVE_FOREGROUND, hcDark: TITLE_BAR_ACTIVE_FOREGROUND, light: TITLE_BAR_ACTIVE_FOREGROUND, hcLight: TITLE_BAR_ACTIVE_FOREGROUND }, localize('titleMenu-foreground', "Foreground color of the title menu"), false ); +colors.registerColor( + 'titleMenu.activeForeground', + { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, + localize('titleMenu-activeForeground', "Active foreground color of the title menu"), + false +); +// background (inactive and active) colors.registerColor( 'titleMenu.background', - { dark: colors.inputForeground, hcDark: colors.inputForeground, light: colors.inputForeground, hcLight: colors.inputForeground }, + { dark: null, hcDark: null, light: null, hcLight: null }, localize('titleMenu-background', "Background color of the title menu"), false ); @@ -107,15 +174,10 @@ const activeBackground = colors.registerColor( localize('titleMenu-activeBackground', "Active background color of the title menu"), false ); +// border: defaults to active background colors.registerColor( 'titleMenu.border', - { dark: activeBackground, hcDark: activeBackground, light: activeBackground, hcLight: activeBackground }, + { dark: activeBackground, hcDark: colors.inputBorder, light: activeBackground, hcLight: colors.inputBorder }, localize('titleMenu-border', "Border color of the title menu"), false ); -colors.registerColor( - 'titleMenu.activeForeground', - { dark: MENUBAR_SELECTION_FOREGROUND, hcDark: MENUBAR_SELECTION_FOREGROUND, light: MENUBAR_SELECTION_FOREGROUND, hcLight: MENUBAR_SELECTION_FOREGROUND }, - localize('titleMenu-activeForeground', "Active foreground color of the title menu"), - false -); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 0a94668c80f..e35c289cb8e 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -195,6 +195,7 @@ export class TitlebarPart extends Part implements ITitleService { const titleMenu = this.instantiationService.createInstance(TitleMenuControl, this.windowTitle); reset(this.title, titleMenu.element); this.titleDisposables.add(titleMenu); + this.titleDisposables.add(titleMenu.onDidChangeVisibility(this.adjustTitleMarginToCenter, this)); } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9b06e86ae1f..9d2c5d0dab0 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -31,7 +31,7 @@ import { isString } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/views'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; import { Command } from 'vs/editor/common/languages'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -54,7 +54,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { focusBorder, listFilterMatchHighlight, listFilterMatchHighlightBorder, textCodeBlockBackground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { FileThemeIcon, FolderThemeIcon, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { CodeDataTransfers, convertResourceUrlsToUriList, DraggedTreeItemsIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; +import { convertResourceUrlsToUriList, DraggedTreeItemsIdentifier, fillEditorsDragData, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; @@ -66,6 +66,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { ThemeSettings } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; +import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; export class TreeViewPane extends ViewPane { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 7bd541c1248..5c9c7d93a9d 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -27,7 +27,7 @@ import { mixin } from 'vs/base/common/objects'; import { Codicon } from 'vs/base/common/codicons'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; export const defaultViewIcon = registerIcon('default-view-icon', Codicon.window, localize('defaultViewIcon', 'Default view icon.')); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts index f3ea2110649..f1e945e1bef 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyPeek.ts @@ -334,8 +334,8 @@ export class CallHierarchyTreePeekWidget extends peekView.PeekViewWidget { } if (fullRange) { this._editor.revealRangeInCenter(fullRange, ScrollType.Immediate); - const ids = this._editor.deltaDecorations([], decorations); - this._previewDisposable.add(toDisposable(() => this._editor.deltaDecorations(ids, []))); + const decorationsCollection = this._editor.createDecorationsCollection(decorations); + this._previewDisposable.add(toDisposable(() => decorationsCollection.clear())); } this._previewDisposable.add(value); diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index ff599441598..5fa3bb659ab 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -15,7 +15,7 @@ import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBo import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon, NLS_NO_RESULTS, NLS_MATCHES_LOCATION } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground, toolbarHoverBackground, toolbarHoverOutline } from 'vs/platform/theme/common/colorRegistry'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground, toolbarHoverBackground, toolbarHoverOutline, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; @@ -359,6 +359,11 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`); } + const hcBorder = theme.getColor(contrastBorder); + if (hcBorder) { + collector.addRule(`.monaco-workbench .simple-find-part { border: 1px solid ${hcBorder}; }`); + } + const error = theme.getColor(errorForeground); if (error) { collector.addRule(`.no-results.matchesCount { color: ${error}; }`); diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index 8a23eaaeb0a..e0fffb28ca8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -239,7 +239,7 @@ class DocumentSymbolsOutline implements IOutline { const { symbol } = entry; this._editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth); - const ids = this._editor.deltaDecorations([], [{ + const decorationsCollection = this._editor.createDecorationsCollection([{ range: symbol.range, options: { description: 'document-symbols-outline-range-highlight', @@ -247,7 +247,7 @@ class DocumentSymbolsOutline implements IOutline { isWholeLine: true } }]); - return toDisposable(() => this._editor.deltaDecorations(ids, [])); + return toDisposable(() => decorationsCollection.clear()); } captureViewState(): IDisposable { diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index d2b4f005a8b..cec3c9c01f8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IQuickAccessRegistry, Extensions as QuickaccesExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; -import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -85,11 +85,6 @@ class GotoLineAction extends Action2 { when: null, primary: KeyMod.CtrlCmd | KeyCode.KeyG, mac: { primary: KeyMod.WinCtrl | KeyCode.KeyG } - }, - menu: { - id: MenuId.TitleMenuQuickPick, - group: '3/editorNav', - order: 2 } }); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index a8b90ea7be3..64e87aef2fc 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -274,10 +274,6 @@ registerAction2(class GotoSymbolAction extends Action2 { id: MenuId.MenubarGoMenu, group: '4_symbol_nav', order: 1 - }, { - id: MenuId.TitleMenuQuickPick, - group: '3/editorNav', - order: 1 }] }); } diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index 7be06df3a41..563ce8a4c9e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -10,6 +10,7 @@ import { IModelDecorationOptions, OverviewRulerLane } from 'vs/editor/common/mod import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { IEditorDecorationsCollection } from 'vs/editor/common/editorCommon'; const overviewRulerDefault = new Color(new RGBA(197, 197, 197, 1)); @@ -19,12 +20,13 @@ export class CommentGlyphWidget { public static description = 'comment-glyph-widget'; private _lineNumber!: number; private _editor: ICodeEditor; - private commentsDecorations: string[] = []; + private readonly _commentsDecorations: IEditorDecorationsCollection; private _commentsOptions: ModelDecorationOptions; constructor(editor: ICodeEditor, lineNumber: number) { this._commentsOptions = this.createDecorationOptions(); this._editor = editor; + this._commentsDecorations = this._editor.createDecorationsCollection(); this.setLineNumber(lineNumber); } @@ -52,13 +54,11 @@ export class CommentGlyphWidget { options: this._commentsOptions }]; - this.commentsDecorations = this._editor.deltaDecorations(this.commentsDecorations, commentsDecorations); + this._commentsDecorations.set(commentsDecorations); } getPosition(): IContentWidgetPosition { - const range = this._editor.hasModel() && this.commentsDecorations && this.commentsDecorations.length - ? this._editor.getModel().getDecorationRange(this.commentsDecorations[0]) - : null; + const range = (this._commentsDecorations.length > 0 ? this._commentsDecorations.getRange(0) : null); return { position: { @@ -70,8 +70,6 @@ export class CommentGlyphWidget { } dispose() { - if (this.commentsDecorations) { - this._editor.deltaDecorations(this.commentsDecorations, []); - } + this._commentsDecorations.clear(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index fa9db7ee660..2d6e03e1c60 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -241,8 +241,10 @@ class CommentingRangeDecorator { }); } - this.decorationIds = editor.deltaDecorations(this.decorationIds, commentingRangeDecorations); - commentingRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + editor.changeDecorations((accessor) => { + this.decorationIds = accessor.deltaDecorations(this.decorationIds, commentingRangeDecorations); + commentingRangeDecorations.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + }); const rangesDifference = this.commentingRangeDecorations.length - commentingRangeDecorations.length; this.commentingRangeDecorations = commentingRangeDecorations; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 4eab6791a7f..943b41f8eb5 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -11,7 +11,7 @@ import severity from 'vs/base/common/severity'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, ITextModel, OverviewRulerLane, IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; +import { IModelDecorationOptions, TrackedRangeStickiness, ITextModel, OverviewRulerLane, IModelDecorationOverviewRulerOptions } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -158,7 +158,7 @@ async function createCandidateDecorations(model: ITextModel, breakpointDecoratio export class BreakpointEditorContribution implements IBreakpointEditorContribution { - private breakpointHintDecoration: string[] = []; + private breakpointHintDecoration: string | null = null; private breakpointWidget: BreakpointWidget | undefined; private breakpointWidgetVisible: IContextKey; private toDispose: IDisposable[] = []; @@ -443,20 +443,21 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi } private ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber: number): void { - const newDecoration: IModelDeltaDecoration[] = []; - if (showBreakpointHintAtLineNumber !== -1) { - newDecoration.push({ - options: breakpointHelperDecoration, - range: { + this.editor.changeDecorations((accessor) => { + if (this.breakpointHintDecoration) { + accessor.removeDecoration(this.breakpointHintDecoration); + this.breakpointHintDecoration = null; + } + if (showBreakpointHintAtLineNumber !== -1) { + this.breakpointHintDecoration = accessor.addDecoration({ startLineNumber: showBreakpointHintAtLineNumber, startColumn: 1, endLineNumber: showBreakpointHintAtLineNumber, endColumn: 1 - } - }); - } - - this.breakpointHintDecoration = this.editor.deltaDecorations(this.breakpointHintDecoration, newDecoration); + }, breakpointHelperDecoration + ); + } + }); } private async setDecorations(): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index b858ddb0659..b09f8c1157c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -109,7 +109,7 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, isFocuse export class CallStackEditorContribution implements IEditorContribution { private toDispose: IDisposable[] = []; - private decorationIds: string[] = []; + private decorations = this.editor.createDecorationsCollection(); constructor( private readonly editor: ICodeEditor, @@ -117,7 +117,7 @@ export class CallStackEditorContribution implements IEditorContribution { @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService, ) { - const setDecorations = () => this.decorationIds = this.editor.deltaDecorations(this.decorationIds, this.createCallStackDecorations()); + const setDecorations = () => this.decorations.set(this.createCallStackDecorations()); this.toDispose.push(Event.any(this.debugService.getViewModel().onDidFocusStackFrame, this.debugService.getModel().onDidChangeCallStack)(() => { setDecorations(); })); @@ -170,7 +170,7 @@ export class CallStackEditorContribution implements IEditorContribution { } dispose(): void { - this.editor.deltaDecorations(this.decorationIds, []); + this.decorations.clear(); this.toDispose = dispose(this.toDispose); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 78e4ec8672c..7a9ec3bbb3d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -34,6 +34,10 @@ import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecyc const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); +export interface IAdapterManagerDelegate { + onDidNewSession: Event; +} + export class AdapterManager extends Disposable implements IAdapterManager { private debuggers: Debugger[]; @@ -46,7 +50,13 @@ export class AdapterManager extends Disposable implements IAdapterManager { private breakpointContributions: Breakpoints[] = []; private debuggerWhenKeys = new Set(); + /** Extensions that were already active before any debugger activation events */ + private earlyActivatedExtensions: Set | undefined; + + private usedDebugTypes = new Set(); + constructor( + delegate: IAdapterManagerDelegate, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -56,7 +66,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { @IContextKeyService private readonly contextKeyService: IContextKeyService, @ILanguageService private readonly languageService: ILanguageService, @IDialogService private readonly dialogService: IDialogService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, + @ILifecycleService private readonly lifecycleService: ILifecycleService ) { super(); this.adapterDescriptorFactories = []; @@ -76,6 +86,10 @@ export class AdapterManager extends Disposable implements IAdapterManager { })); this.lifecycleService.when(LifecyclePhase.Eventually) .then(() => this.debugExtensionsAvailable.set(this.debuggers.length > 0)); // If no extensions with a debugger contribution are loaded + + this._register(delegate.onDidNewSession(s => { + this.usedDebugTypes.add(s.configuration.type); + })); } private registerListeners(): void { @@ -284,18 +298,18 @@ export class AdapterManager extends Disposable implements IAdapterManager { return this.debuggers.find(dbg => strings.equalsIgnoreCase(dbg.type, type)); } + getEnabledDebugger(type: string): Debugger | undefined { + const adapter = this.getDebugger(type); + return adapter && adapter.enabled ? adapter : undefined; + } + isDebuggerInterestedInLanguage(language: string): boolean { return !!this.debuggers .filter(d => d.enabled) .find(a => language && a.languages && a.languages.indexOf(language) >= 0); } - async guessDebugger(gettingConfigurations: boolean, type?: string): Promise { - if (type) { - const adapter = this.getDebugger(type); - return adapter && adapter.enabled ? adapter : undefined; - } - + async guessDebugger(gettingConfigurations: boolean): Promise { const activeTextEditorControl = this.editorService.activeTextEditorControl; let candidates: Debugger[] = []; let languageLabel: string | null = null; @@ -326,10 +340,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { .filter(dbg => dbg.hasInitialConfiguration() || dbg.hasConfigurationProvider()); } - candidates.sort((first, second) => first.label.localeCompare(second.label)); - const picks: { label: string; debugger?: Debugger; type?: string }[] = candidates.map(c => ({ label: c.label, debugger: c })); - - if (picks.length === 0 && languageLabel) { + if (candidates.length === 0 && languageLabel) { if (languageLabel.indexOf(' ') >= 0) { languageLabel = `'${languageLabel}'`; } @@ -342,10 +353,45 @@ export class AdapterManager extends Disposable implements IAdapterManager { return undefined; } - picks.push({ type: 'separator', label: '' }); - const placeHolder = nls.localize('selectDebug', "Select debugger"); + this.initExtensionActivationsIfNeeded(); - picks.push({ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }); + candidates.sort((first, second) => first.label.localeCompare(second.label)); + + const suggestedCandidates: Debugger[] = []; + const otherCandidates: Debugger[] = []; + candidates.forEach(d => { + const descriptor = d.getMainExtensionDescriptor(); + if (descriptor.id && !!this.earlyActivatedExtensions?.has(descriptor.id)) { + // Was activated early + suggestedCandidates.push(d); + } else if (this.usedDebugTypes.has(d.type)) { + // Was used already + suggestedCandidates.push(d); + } else { + otherCandidates.push(d); + } + }); + + const picks: { label: string; debugger?: Debugger; type?: string }[] = []; + if (suggestedCandidates.length > 0) { + picks.push( + { type: 'separator', label: nls.localize('suggestedDebuggers', "Suggested") }, + ...suggestedCandidates.map(c => ({ label: c.label, debugger: c }))); + } + + if (otherCandidates.length > 0) { + if (picks.length > 0) { + picks.push({ type: 'separator', label: '' }); + } + + picks.push(...otherCandidates.map(c => ({ label: c.label, debugger: c }))); + } + + picks.push( + { type: 'separator', label: '' }, + { label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }); + + const placeHolder = nls.localize('selectDebug', "Select debugger"); return this.quickInputService.pick<{ label: string; debugger?: Debugger }>(picks, { activeItem: picks[0], placeHolder }) .then(picked => { if (picked && picked.debugger) { @@ -358,7 +404,22 @@ export class AdapterManager extends Disposable implements IAdapterManager { }); } + private initExtensionActivationsIfNeeded(): void { + if (!this.earlyActivatedExtensions) { + this.earlyActivatedExtensions = new Set(); + + const status = this.extensionService.getExtensionsStatus(); + for (const id in status) { + if (!!status[id].activationTimes) { + this.earlyActivatedExtensions.add(id); + } + } + } + } + async activateDebuggers(activationEvent: string, debugType?: string): Promise { + this.initExtensionActivationsIfNeeded(); + const promises: Promise[] = [ this.extensionService.activateByEvent(activationEvent), this.extensionService.activateByEvent('onDebug') diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index e3eaf0b1438..0245dbd5a2d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -119,7 +119,7 @@ export class ConfigurationManager implements IConfigurationManager { } async resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise { - await this.activateDebuggers('onDebugResolve', type); + await this.adapterManager.activateDebuggers('onDebugResolve', type); // pipe the config through the promises sequentially. Append at the end the '*' types const providers = this.configProviders.filter(p => p.type === type && p.resolveDebugConfiguration) .concat(this.configProviders.filter(p => p.type === '*' && p.resolveDebugConfiguration)); @@ -134,7 +134,7 @@ export class ConfigurationManager implements IConfigurationManager { // The resolver can change the type, ensure activation happens, #135090 if (result?.type && result.type !== config.type) { - await this.activateDebuggers('onDebugResolve', result.type); + await this.adapterManager.activateDebuggers('onDebugResolve', result.type); } return result; @@ -157,7 +157,7 @@ export class ConfigurationManager implements IConfigurationManager { } async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise { - await this.activateDebuggers('onDebugInitialConfigurations'); + await this.adapterManager.activateDebuggers('onDebugInitialConfigurations'); const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Initial && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token))); return results.reduce((first, second) => first.concat(second), []); @@ -197,13 +197,13 @@ export class ConfigurationManager implements IConfigurationManager { return { label: this.adapterManager.getDebuggerLabel(type)!, getProvider: async () => { - await this.activateDebuggers(onDebugDynamicConfigurationsName, type); + await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type); return this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations); }, type, pick: async () => { // Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578 - await this.activateDebuggers(onDebugDynamicConfigurationsName, type); + await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type); const disposables = new DisposableStore(); const input = disposables.add(this.quickInputService.createQuickPick()); input.busy = true; @@ -449,17 +449,6 @@ export class ConfigurationManager implements IConfigurationManager { } } - async activateDebuggers(activationEvent: string, debugType?: string): Promise { - const promises: Promise[] = [ - this.extensionService.activateByEvent(activationEvent), - this.extensionService.activateByEvent('onDebug') - ]; - if (debugType) { - promises.push(this.extensionService.activateByEvent(`${activationEvent}:${debugType}`)); - } - await Promise.all(promises); - } - private setSelectedLaunchName(selectedName: string | undefined): void { this.selectedName = selectedName; @@ -534,7 +523,7 @@ abstract class AbstractLaunch { async getInitialConfigurationContent(folderUri?: uri, type?: string, token?: CancellationToken): Promise { let content = ''; - const adapter = await this.adapterManager.guessDebugger(true, type); + const adapter = type ? this.adapterManager.getEnabledDebugger(type) : await this.adapterManager.guessDebugger(true); if (adapter) { const initialConfigs = await this.configurationManager.provideDebugConfigurations(folderUri, adapter.type, token || CancellationToken.None); content = await adapter.getInitialConfigurationContent(initialConfigs); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 848242efad6..98bcc2d1d6f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -221,7 +221,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private configurationWidget: FloatingClickWidget | undefined; private altListener: IDisposable | undefined; private altPressed = false; - private oldDecorations: string[] = []; + private oldDecorations = this.editor.createDecorationsCollection(); private readonly debounceInfo: IFeatureDebounceInformation; constructor( @@ -605,7 +605,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private get removeInlineValuesScheduler(): RunOnceScheduler { return new RunOnceScheduler( () => { - this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, []); + this.oldDecorations.clear(); }, 100 ); @@ -759,7 +759,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { decoration => `${decoration.range.startLineNumber}:${decoration?.options.after?.content}`); } - this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, allDecorations); + this.oldDecorations.set(allDecorations); } dispose(): void { @@ -771,6 +771,6 @@ export class DebugEditorContribution implements IDebugEditorContribution { } this.toDispose = dispose(this.toDispose); - this.oldDecorations = this.editor.deltaDecorations(this.oldDecorations, []); + this.oldDecorations.clear(); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 66626216c50..21d65a90cb3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -77,7 +77,7 @@ export class DebugHoverWidget implements IContentWidget { private tree!: AsyncDataTree; private showAtPosition: Position | null; private positionPreference: ContentWidgetPositionPreference[]; - private highlightDecorations: string[]; + private readonly highlightDecorations = this.editor.createDecorationsCollection(); private complexValueContainer!: HTMLElement; private complexValueTitle!: HTMLElement; private valueContainer!: HTMLElement; @@ -96,7 +96,6 @@ export class DebugHoverWidget implements IContentWidget { this._isVisible = false; this.showAtPosition = null; - this.highlightDecorations = []; this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]; } @@ -264,7 +263,7 @@ export class DebugHoverWidget implements IContentWidget { } if (rng) { - this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{ + this.highlightDecorations.set([{ range: rng, options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); @@ -351,8 +350,7 @@ export class DebugHoverWidget implements IContentWidget { this.editor.focus(); } this._isVisible = false; - this.editor.deltaDecorations(this.highlightDecorations, []); - this.highlightDecorations = []; + this.highlightDecorations.clear(); this.editor.layoutContentWidget(this); this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]; } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 224b72fe4fa..50c7f78b6b2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -114,7 +114,7 @@ export class DebugService implements IDebugService { this._onWillNewSession = new Emitter(); this._onDidEndSession = new Emitter(); - this.adapterManager = this.instantiationService.createInstance(AdapterManager); + this.adapterManager = this.instantiationService.createInstance(AdapterManager, { onDidNewSession: this.onDidNewSession }); this.disposables.add(this.adapterManager); this.configurationManager = this.instantiationService.createInstance(ConfigurationManager, this.adapterManager); this.disposables.add(this.configurationManager); diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 577a80c532e..c061b736c55 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -70,6 +70,12 @@ /* Expressions */ + +.monaco-workbench .debug-pane .monaco-list-row .expression, +.monaco-workbench .debug-hover-widget .monaco-list-row .expression { + display: flex; +} + .monaco-workbench .debug-pane .monaco-list-row .expression, .monaco-workbench .debug-hover-widget .monaco-list-row .expression { font-size: 13px; @@ -90,6 +96,13 @@ .monaco-workbench .monaco-list-row .expression .lazy-button { margin-left: 3px; + display: none; + border-radius: 5px; + align-self: center; +} + +.monaco-workbench .monaco-list-row .expression.lazy .lazy-button { + display: inline; } /* Links */ @@ -120,16 +133,6 @@ font-style: italic; } -.monaco-workbench .monaco-list-row .expression .lazy-button { - display: none; - border-radius: 5px; - padding: 3px; -} - -.monaco-workbench .monaco-list-row .expression.lazy .lazy-button { - display: inline; -} - .monaco-workbench .debug-inline-value { background-color: var(--vscode-editor-inlineValuesBackground); color: var(--vscode-editor-inlineValuesForeground); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 493c9270e85..57a946206a8 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -216,10 +216,6 @@ font-size: 11px; } -.debug-pane .monaco-list-row .expression { - display: flex; -} - .debug-pane .monaco-list-row .expression .actionbar-spacer { flex-grow: 1; } diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 8a41ba6c603..531f7f789cc 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -225,6 +225,7 @@ export class Debugger implements IDebugger { const properties = attributes.properties; properties['type'] = { enum: [this.type], + enumDescriptions: [this.label], description: nls.localize('debugType', "Type of configuration."), pattern: '^(?!node2)', deprecationMessage: this.enabled ? undefined : debuggerDisabledMessage(this.type), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index ba209ca06a9..2f1244e6ebd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -658,7 +658,7 @@ export class ExtensionEditor extends EditorPane { } }; reset(template.recommendation); - if (extension.state === ExtensionState.Installed) { + if (extension.deprecated || extension.state === ExtensionState.Installed) { return; } updateRecommendationText(false); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 6e4013f7272..d3b1573086e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -250,6 +250,7 @@ export abstract class AbstractInstallAction extends ExtensionAction { @IExtensionService private readonly runtimeExtensionService: IExtensionService, @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @ILabelService private readonly labelService: ILabelService, + @IDialogService private readonly dialogService: IDialogService, ) { super(id, localize('install', "Install"), cssClass, false); this.update(); @@ -274,6 +275,19 @@ export abstract class AbstractInstallAction extends ExtensionAction { if (!this.extension) { return; } + + if (this.extension.deprecated && isBoolean(this.extension.deprecated)) { + const result = await this.dialogService.confirm({ + type: 'warning', + title: localize('install confirmation', "Are you sure you want to install '{0}'?", this.extension.displayName), + message: localize('deprecated message', "This extension is no longer being maintained and is deprecated."), + primaryButton: localize('install anyway', "Install Anyway"), + }); + if (!result.confirmed) { + return; + } + } + this.extensionsWorkbenchService.open(this.extension, { showPreReleaseVersion: this.installPreReleaseVersion }); alert(localize('installExtensionStart', "Installing extension {0} started. An editor is now open with more details on this extension", this.extension.displayName)); @@ -373,12 +387,13 @@ export class InstallAction extends AbstractInstallAction { @IExtensionService runtimeExtensionService: IExtensionService, @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @ILabelService labelService: ILabelService, + @IDialogService dialogService: IDialogService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IWorkbenchExtensionManagementService private readonly workbenchExtensioManagementService: IWorkbenchExtensionManagementService, @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, ) { super(`extensions.install`, installPreReleaseVersion, InstallAction.Class, - extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService); + extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService); this.updateLabel(); this._register(labelService.onDidChangeFormatters(() => this.updateLabel(), this)); this._register(Event.any(userDataSyncEnablementService.onDidChangeEnablement, @@ -438,11 +453,12 @@ export class InstallAndSyncAction extends AbstractInstallAction { @IExtensionService runtimeExtensionService: IExtensionService, @IWorkbenchThemeService workbenchThemeService: IWorkbenchThemeService, @ILabelService labelService: ILabelService, + @IDialogService dialogService: IDialogService, @IProductService productService: IProductService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, ) { super('extensions.installAndSync', installPreReleaseVersion, AbstractInstallAction.Class, - extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService); + extensionsWorkbenchService, instantiationService, runtimeExtensionService, workbenchThemeService, labelService, dialogService); this.tooltip = localize({ key: 'install everywhere tooltip', comment: ['Placeholder is the name of the product. Eg: Visual Studio Code or Visual Studio Code - Insiders'] }, "Install this extension in all your synced {0} instances", productService.nameLong); this._register(Event.any(userDataSyncEnablementService.onDidChangeEnablement, Event.filter(userDataSyncEnablementService.onDidChangeResourceEnablement, e => e[0] === SyncResource.Extensions))(() => this.update())); @@ -2156,13 +2172,13 @@ export class ExtensionStatusAction extends ExtensionAction { return; } - if (this.extension.isUnsupported) { - if (isBoolean(this.extension.isUnsupported)) { - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported tooltip', "This extension no longer supported.")) }, true); + if (this.extension.deprecated) { + if (isBoolean(this.extension.deprecated)) { + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported tooltip', "This extension is no longer being maintained and is deprecated.")) }, true); } else { - const link = `[${this.extension.isUnsupported.preReleaseExtension.displayName}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.isUnsupported.preReleaseExtension.id]))}`)})`; + const link = `[${this.extension.deprecated.displayName}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.deprecated.id]))}`)})`; if (this.extension.state !== ExtensionState.Installed) { - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported prerelease switch tooltip', "This extension is now part of the {0} extension as a pre-release version.", link)) }, true); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('unsupported prerelease switch tooltip', "This extension has been deprecated. Use {0} instead.", link)) }, true); } } return; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 9a31eb974cc..4755c672581 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -188,7 +188,7 @@ export class Renderer implements IPagedRenderer { const updateEnablement = async () => { let isDisabled = false; if (extension.state === ExtensionState.Uninstalled) { - isDisabled = !(await this.extensionsWorkbenchService.canInstall(extension)); + isDisabled = !!extension.deprecated || !(await this.extensionsWorkbenchService.canInstall(extension)); } else if (extension.local && !isLanguagePackExtension(extension.local.manifest)) { const runningExtensions = await this.extensionService.getExtensions(); const runningExtension = runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, extension.identifier))[0]; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 99b7c3eb1f1..0d895be2eae 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -58,7 +58,7 @@ import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/act import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { coalesce } from 'vs/base/common/arrays'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd'; import { extname } from 'vs/base/common/resources'; const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 9b38a120296..e4592383fe6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -185,7 +185,7 @@ export class RecommendationWidget extends ExtensionWidget { render(): void { this.clear(); - if (!this.extension || this.extension.state === ExtensionState.Installed) { + if (!this.extension || this.extension.state === ExtensionState.Installed || this.extension.deprecated) { return; } const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason(); @@ -549,6 +549,9 @@ export class ExtensionHoverWidget extends ExtensionWidget { if (extension.state === ExtensionState.Installed) { return undefined; } + if (extension.deprecated) { + return undefined; + } const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]; if (!recommendation?.reasonText) { return undefined; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 8f96b5e7fcf..7fda957ef46 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -34,7 +34,7 @@ import * as resources from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform } from 'vs/platform/extensions/common/extensions'; +import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, TargetPlatform, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { IProductService } from 'vs/platform/product/common/productService'; import { FileAccess } from 'vs/base/common/network'; @@ -46,11 +46,21 @@ import { IExtensionManifestPropertiesService } from 'vs/workbench/services/exten import { IExtensionService, IExtensionsStatus } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; import { isWeb } from 'vs/base/common/platform'; +import { GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; interface IExtensionStateProvider { (extension: Extension): T; } +interface InstalledExtensionsEvent { + readonly extensionIds: string; + readonly count: number; +} +interface ExtensionsLoadClassification extends GDPRClassification { + readonly extensionIds: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; + readonly count: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight' }; +} + export class Extension implements IExtension { public enablementState: EnablementState = EnablementState.EnabledGlobally; @@ -188,7 +198,7 @@ export class Extension implements IExtension { } public isMalicious: boolean = false; - public isUnsupported: boolean | { preReleaseExtension: { id: string; displayName: string } } = false; + public deprecated: boolean | { id: string; displayName: string } = false; get installCount(): number | undefined { return this.gallery ? this.gallery.installCount : undefined; @@ -390,17 +400,9 @@ class Extensions extends Disposable { static updateExtensionFromControlManifest(extension: Extension, extensionsControlManifest: IExtensionsControlManifest): void { const isMalicious = extensionsControlManifest.malicious.some(identifier => areSameExtensions(extension.identifier, identifier)); - if (extension.isMalicious !== isMalicious) { - extension.isMalicious = isMalicious; - } - const unsupportedPreRelease = extensionsControlManifest.unsupportedPreReleaseExtensions ? extensionsControlManifest.unsupportedPreReleaseExtensions[extension.identifier.id.toLowerCase()] : undefined; - if (unsupportedPreRelease) { - if (isBoolean(extension.isUnsupported) || !areSameExtensions({ id: extension.isUnsupported.preReleaseExtension.id }, { id: unsupportedPreRelease.id })) { - extension.isUnsupported = { preReleaseExtension: unsupportedPreRelease }; - } - } else if (extension.isUnsupported) { - extension.isUnsupported = false; - } + extension.isMalicious = isMalicious; + const deprecated = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[extension.identifier.id.toLowerCase()] : undefined; + extension.deprecated = deprecated ?? false; } private readonly _onChange: Emitter<{ extension: Extension; operation?: InstallOperation } | undefined> = this._register(new Emitter<{ extension: Extension; operation?: InstallOperation } | undefined>()); @@ -724,6 +726,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.queryLocal().then(() => { this.resetIgnoreAutoUpdateExtensions(); this.eventuallyCheckForUpdates(true); + this._reportTelemetry(); // Always auto update builtin extensions in web if (isWeb && !this.isAutoUpdateEnabled()) { this.autoUpdateBuiltinExtensions(); @@ -735,6 +738,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.updateActivity(); })); } + private _reportTelemetry() { + const extensionIds = this.installed.filter(extension => + extension.type === ExtensionType.User && + (extension.enablementState === EnablementState.EnabledWorkspace || + extension.enablementState === EnablementState.EnabledGlobally)) + .map(extension => ExtensionIdentifier.toKey(extension.identifier.id)); + this.telemetryService.publicLog2('installedExtensions', { extensionIds: extensionIds.join(';'), count: extensionIds.length }); + } get local(): IExtension[] { const byId = groupByExtension(this.installed, r => r.identifier); @@ -1155,7 +1166,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return false; } - if (extension.isUnsupported) { + if (extension.deprecated && !isBoolean(extension.deprecated)) { return false; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 76f1c03de8a..78e7107e67c 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -149,6 +149,12 @@ .extension-list-item > .details > .description { padding-right: 11px; + color: var(--vscode-descriptionForeground); +} + +.hc-black .extension-list-item > .details > .description, +.hc-light .extension-list-item > .details > .description { + color: unset; } .extension-list-item > .details > .footer { diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index b3846d038fc..23744cd4f9b 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -78,7 +78,7 @@ export interface IExtension { readonly local?: ILocalExtension; gallery?: IGalleryExtension; readonly isMalicious: boolean; - readonly isUnsupported: boolean | { preReleaseExtension: { id: string; displayName: string } }; + readonly deprecated: boolean | { id: string; displayName: string }; } export const SERVICE_ID = 'extensionsWorkbenchService'; diff --git a/src/vs/workbench/contrib/files/browser/fileImportExport.ts b/src/vs/workbench/contrib/files/browser/fileImportExport.ts index 71eda39201c..2215ecd93ad 100644 --- a/src/vs/workbench/contrib/files/browser/fileImportExport.ts +++ b/src/vs/workbench/contrib/files/browser/fileImportExport.ts @@ -20,7 +20,7 @@ import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { extractEditorsDropData } from 'vs/workbench/browser/dnd'; +import { extractEditorsDropData } from 'vs/platform/dnd/browser/dnd'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { isWeb } from 'vs/base/common/platform'; import { triggerDownload } from 'vs/base/browser/dom'; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 1181f9867c4..a24f390cf51 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -30,7 +30,8 @@ import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { compareFileExtensionsDefault, compareFileNamesDefault, compareFileNamesUpper, compareFileExtensionsUpper, compareFileNamesLower, compareFileExtensionsLower, compareFileNamesUnicode, compareFileExtensionsUnicode } from 'vs/base/common/comparers'; -import { fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; +import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 06ba5acdbde..fdfcc0a46a0 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -32,7 +32,8 @@ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/m import { IMenuService, MenuId, IMenu, Action2, registerAction2, MenuRegistry } from 'vs/platform/actions/common/actions'; import { OpenEditorsDirtyEditorContext, OpenEditorsGroupContext, OpenEditorsReadonlyEditorContext, SAVE_ALL_LABEL, SAVE_ALL_COMMAND_ID, NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser/fileConstants'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; -import { ResourcesDropHandler, fillEditorsDragData, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; +import { ResourcesDropHandler, fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 11b51e17e2f..d602602277f 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -241,16 +241,18 @@ export abstract class BaseCellViewModel extends Disposable { writeTransientState(editor.getModel(), this._editorTransientState, this._codeEditorService); } - this._resolvedDecorations.forEach((value, key) => { - if (key.startsWith('_lazy_')) { - // lazy ones - const ret = this._textEditor!.deltaDecorations([], [value.options]); - this._resolvedDecorations.get(key)!.id = ret[0]; - } - else { - const ret = this._textEditor!.deltaDecorations([], [value.options]); - this._resolvedDecorations.get(key)!.id = ret[0]; - } + this._textEditor.changeDecorations((accessor) => { + this._resolvedDecorations.forEach((value, key) => { + if (key.startsWith('_lazy_')) { + // lazy ones + const ret = accessor.addDecoration(value.options.range, value.options.options); + this._resolvedDecorations.get(key)!.id = ret; + } + else { + const ret = accessor.addDecoration(value.options.range, value.options.options); + this._resolvedDecorations.get(key)!.id = ret; + } + }); }); this._editorListeners.push(this._textEditor.onDidChangeCursorSelection(() => { this._onDidChangeState.fire({ selectionChanged: true }); })); @@ -263,12 +265,14 @@ export abstract class BaseCellViewModel extends Disposable { this.saveViewState(); this.saveTransientState(); // decorations need to be cleared first as editors can be resued. - this._resolvedDecorations.forEach(value => { - const resolvedid = value.id; + this._textEditor?.changeDecorations((accessor) => { + this._resolvedDecorations.forEach(value => { + const resolvedid = value.id; - if (resolvedid) { - this._textEditor?.deltaDecorations([resolvedid], []); - } + if (resolvedid) { + accessor.removeDecoration(resolvedid); + } + }); }); this._textEditor = undefined; @@ -332,16 +336,21 @@ export abstract class BaseCellViewModel extends Disposable { return decorationId; } - const result = this._textEditor.deltaDecorations([], [decoration]); - this._resolvedDecorations.set(result[0], { id: result[0], options: decoration }); - return result[0]; + let id: string; + this._textEditor.changeDecorations((accessor) => { + id = accessor.addDecoration(decoration.range, decoration.options); + this._resolvedDecorations.set(id, { id, options: decoration }); + }); + return id!; } removeModelDecoration(decorationId: string) { const realDecorationId = this._resolvedDecorations.get(decorationId); if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) { - this._textEditor.deltaDecorations([realDecorationId.id!], []); + this._textEditor.changeDecorations((accessor) => { + accessor.removeDecoration(realDecorationId.id!); + }); } // lastly, remove all the cache diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts index 814f9e1282a..e69f1504400 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditorContribution.ts @@ -163,7 +163,7 @@ export class KeybindingWidgetRenderer extends Disposable { export class KeybindingEditorDecorationsRenderer extends Disposable { private _updateDecorations: RunOnceScheduler; - private _dec: string[] = []; + private readonly _dec = this._editor.createDecorationsCollection(); constructor( private _editor: ICodeEditor, @@ -178,7 +178,7 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { this._register(this._keybindingService.onDidUpdateKeybindings((e) => this._updateDecorations.schedule())); this._register({ dispose: () => { - this._dec = this._editor.deltaDecorations(this._dec, []); + this._dec.clear(); this._updateDecorations.cancel(); } }); @@ -201,7 +201,7 @@ export class KeybindingEditorDecorationsRenderer extends Disposable { } } - this._dec = this._editor.deltaDecorations(this._dec, newDecorations); + this._dec.set(newDecorations); } private _getDecorationForEntry(model: ITextModel, entry: Node): IModelDeltaDecoration | null { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index 4d2b985ec74..a2aa81b94c1 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -694,7 +694,7 @@ class UnsupportedSettingsRenderer extends Disposable implements languages.CodeAc class WorkspaceConfigurationRenderer extends Disposable { private static readonly supportedKeys = ['folders', 'tasks', 'launch', 'extensions', 'settings', 'remoteAuthority', 'transient']; - private decorationIds: string[] = []; + private readonly decorations = this.editor.createDecorationsCollection(); private renderingDelayer: Delayer = new Delayer(200); constructor(private editor: ICodeEditor, private workspaceSettingsEditorModel: SettingsEditorModel, @@ -723,7 +723,7 @@ class WorkspaceConfigurationRenderer extends Disposable { } } } - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, ranges.map(range => this.createDecoration(range))); + this.decorations.set(ranges.map(range => this.createDecoration(range))); } if (markerData.length) { this.markerService.changeOne('WorkspaceConfigurationRenderer', this.workspaceSettingsEditorModel.uri, markerData); @@ -747,7 +747,7 @@ class WorkspaceConfigurationRenderer extends Disposable { override dispose(): void { this.markerService.remove('WorkspaceConfigurationRenderer', [this.workspaceSettingsEditorModel.uri]); - this.decorationIds = this.editor.deltaDecorations(this.decorationIds, []); + this.decorations.clear(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index 4c942330f52..f74ec72414c 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -518,14 +518,13 @@ export class EditPreferenceWidget extends Disposable { private _line: number = -1; private _preferences: T[] = []; - private _editPreferenceDecoration: string[]; + private readonly _editPreferenceDecoration = this.editor.createDecorationsCollection(); private readonly _onClick = this._register(new Emitter()); readonly onClick: Event = this._onClick.event; constructor(private editor: ICodeEditor) { super(); - this._editPreferenceDecoration = []; this._register(this.editor.onMouseDown((e: IEditorMouseEvent) => { if (e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.detail.isAfterLines || !this.isVisible()) { return; @@ -560,11 +559,11 @@ export class EditPreferenceWidget extends Disposable { endColumn: 1 } }); - this._editPreferenceDecoration = this.editor.deltaDecorations(this._editPreferenceDecoration, newDecoration); + this._editPreferenceDecoration.set(newDecoration); } hide(): void { - this._editPreferenceDecoration = this.editor.deltaDecorations(this._editPreferenceDecoration, []); + this._editPreferenceDecoration.clear(); } isVisible(): boolean { diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 0f8b4edc30d..08223e5257f 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -166,12 +166,7 @@ export class ShowAllCommandsAction extends Action2 { primary: !isFirefox ? (KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyP) : undefined, secondary: [KeyCode.F1] }, - f1: true, - menu: { - id: MenuId.TitleMenuQuickPick, - group: '1/workspaceNav', - order: 3 - } + f1: true }); } diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index eb62a41277f..a2d8b9671ae 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -31,6 +31,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IDownloadService } from 'vs/platform/download/common/download'; import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc'; +import { timeout } from 'vs/base/common/async'; export class LabelContribution implements IWorkbenchContribution { constructor( @@ -165,6 +166,9 @@ class RemoteInvalidWorkspaceDetector extends Disposable implements IWorkbenchCon } } +const EXT_HOST_LATENCY_SAMPLES = 5; +const EXT_HOST_LATENCY_DELAY = 2_000; + class InitialRemoteConnectionHealthContribution implements IWorkbenchContribution { constructor( @@ -185,17 +189,22 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio owner: 'alexdima'; comment: 'The initial connection succeeded'; web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + connectionTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time, in ms, until connected'; isMeasurement: true }; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; }; type RemoteConnectionSuccessEvent = { web: boolean; + connectionTimeMs: number | undefined; remoteName: string | undefined; }; this._telemetryService.publicLog2('remoteConnectionSuccess', { web: isWeb, + connectionTimeMs: await this._remoteAgentService.getConnection()?.getInitialConnectionTimeMs(), remoteName: getRemoteName(this._environmentService.remoteAuthority) }); + await this._measureExtHostLatency(); + } catch (err) { type RemoteConnectionFailureClassification = { @@ -203,21 +212,56 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio comment: 'The initial connection failed'; web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; + connectionTimeMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Time, in ms, until connection failure'; isMeasurement: true }; message: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth' }; }; type RemoteConnectionFailureEvent = { web: boolean; remoteName: string | undefined; + connectionTimeMs: number | undefined; message: string; }; this._telemetryService.publicLog2('remoteConnectionFailure', { web: isWeb, + connectionTimeMs: await this._remoteAgentService.getConnection()?.getInitialConnectionTimeMs(), remoteName: getRemoteName(this._environmentService.remoteAuthority), message: err ? err.message : '' }); } } + + private async _measureExtHostLatency() { + // Get the minimum latency, since latency spikes could be caused by a busy extension host. + let bestLatency = Infinity; + for (let i = 0; i < EXT_HOST_LATENCY_SAMPLES; i++) { + const rtt = await this._remoteAgentService.getRoundTripTime(); + if (rtt === undefined) { + return; + } + bestLatency = Math.min(bestLatency, rtt / 2); + await timeout(EXT_HOST_LATENCY_DELAY); + } + + type RemoteConnectionFailureClassification = { + owner: 'connor4312'; + comment: 'The latency to the remote extension host'; + web: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether this is running on web' }; + remoteName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Anonymized remote name' }; + latencyMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Latency to the remote, in milliseconds'; isMeasurement: true }; + }; + type RemoteConnectionFailureEvent = { + web: boolean; + remoteName: string | undefined; + latencyMs: number; + }; + + this._telemetryService.publicLog2('remoteConnectionFailure', { + web: isWeb, + remoteName: getRemoteName(this._environmentService.remoteAuthority), + latencyMs: bestLatency + }); + } } const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e7d863ed61c..a3059234eb1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -85,6 +85,7 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/mar import { Button, ButtonWithDescription } from 'vs/base/browser/ui/button/button'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; +import { DropIntoEditorController } from 'vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -1935,7 +1936,8 @@ class SCMInputWidget extends Disposable { quickSuggestions: false, scrollbar: { alwaysConsumeMouseWheel: false }, overflowWidgetsDomNode, - renderWhitespace: 'none' + renderWhitespace: 'none', + enableDropIntoEditor: true, }; const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { @@ -1948,7 +1950,8 @@ class SCMInputWidget extends Disposable { ContextMenuController.ID, ColorDetector.ID, ModesHoverController.ID, - LinkDetector.ID + LinkDetector.ID, + DropIntoEditorController.ID ]) }; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index d7973cf1b18..2a79eacad3b 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -609,11 +609,6 @@ registerAction2(class ShowAllSymbolsAction extends Action2 { original: 'Go to Symbol in Workspace...' }, f1: true, - menu: { - id: MenuId.TitleMenuQuickPick, - group: '1/workspaceNav', - order: 2 - }, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KeyT diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index c368c210ffd..578221a80ef 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -264,7 +264,9 @@ export class FileMatch extends Disposable implements IFileMatch { private unbindModel(): void { if (this._model) { this._updateScheduler.cancel(); - this._model.deltaDecorations(this._modelDecorations, []); + this._model.changeDecorations((accessor) => { + this._modelDecorations = accessor.deltaDecorations(this._modelDecorations, []); + }); this._model = null; this._modelListener!.dispose(); } @@ -335,14 +337,17 @@ export class FileMatch extends Disposable implements IFileMatch { return; } - if (this.parent().showHighlights) { - this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, this.matches().map(match => { - range: match.range(), - options: FileMatch.getDecorationOption(this.isMatchSelected(match)) - })); - } else { - this._modelDecorations = this._model.deltaDecorations(this._modelDecorations, []); - } + this._model.changeDecorations((accessor) => { + const newDecorations = ( + this.parent().showHighlights + ? this.matches().map(match => { + range: match.range(), + options: FileMatch.getDecorationOption(this.isMatchSelected(match)) + }) + : [] + ); + this._modelDecorations = accessor.deltaDecorations(this._modelDecorations, newDecorations); + }); } id(): string { @@ -1222,7 +1227,10 @@ export class RangeHighlightDecorations implements IDisposable { removeHighlightRange() { if (this._model && this._decorationId) { - this._model.deltaDecorations([this._decorationId], []); + const decorationId = this._decorationId; + this._model.changeDecorations((accessor) => { + accessor.removeDecoration(decorationId); + }); } this._decorationId = null; } @@ -1242,7 +1250,9 @@ export class RangeHighlightDecorations implements IDisposable { private doHighlightRange(model: ITextModel, range: Range) { this.removeHighlightRange(); - this._decorationId = model.deltaDecorations([], [{ range: range, options: RangeHighlightDecorations._RANGE_HIGHLIGHT_DECORATION }])[0]; + model.changeDecorations((accessor) => { + this._decorationId = accessor.addDecoration(range, RangeHighlightDecorations._RANGE_HIGHLIGHT_DECORATION); + }); this.setModel(model); } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 02867a99753..945161cad7f 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -458,3 +458,9 @@ .hc-light .xterm-find-result-decoration { outline-style: dotted !important; } + +.hc-black .xterm-find-active-result-decoration, +.hc-light .xterm-find-active-result-decoration { + outline-style: solid !important; + outline-width: 2px !important; +} diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index cc7b5d1b6d5..ab808bab234 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -37,7 +37,8 @@ export class RemotePty extends Disposable implements ITerminalChildProcess { shellType: undefined, hasChildProcesses: true, resolvedShellLaunchConfig: {}, - overrideDimensions: undefined + overrideDimensions: undefined, + failedShellIntegrationActivation: false }; get id(): number { return this._id; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index f8aae95de73..104e9f455e1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -16,7 +16,7 @@ import { KeybindingWeight, KeybindingsRegistry, IKeybindings } from 'vs/platform import { Registry } from 'vs/platform/registry/common/platform'; import { getQuickNavigateHandler } from 'vs/workbench/browser/quickaccess'; import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; -import { Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput } from 'vs/workbench/browser/dnd'; +import { Extensions as DragAndDropExtensions, IDragAndDropContributionRegistry, IDraggedResourceEditorInput } from 'vs/platform/dnd/browser/dnd'; import { registerTerminalActions, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; import { TERMINAL_VIEW_ID, TerminalCommandId, ITerminalProfileService } from 'vs/workbench/contrib/terminal/common/terminal'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 7a9ca118355..6d1494b10d3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -862,6 +862,8 @@ export interface IXtermTerminal { */ readonly shellIntegration: IShellIntegration; + readonly onDidChangeSelection: Event; + /** * The position of the terminal. */ @@ -906,6 +908,11 @@ export interface IXtermTerminal { * Clears the search result decorations */ clearSearchDecorations(): void; + + /** + * Clears the active search result decorations + */ + clearActiveSearchDecoration(): void; } export interface IRequestAddInstanceToGroupEvent { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 4ba93e51fb3..ec9e1af2191 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -7,12 +7,14 @@ import { SimpleFindWidget } from 'vs/workbench/contrib/codeEditor/browser/find/s import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; -import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalGroupService, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Event } from 'vs/base/common/event'; +import { ISearchOptions } from 'xterm-addon-search'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey; @@ -50,23 +52,24 @@ export class TerminalFindWidget extends SimpleFindWidget { } find(previous: boolean, update?: boolean) { - const instance = this._terminalService.activeInstance; - if (!instance) { + const xterm = this._terminalService.activeInstance?.xterm; + if (!xterm) { return; } if (previous) { - instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: update }); + this._findPreviousWithEvent(xterm, this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: update }); } else { - instance.xterm?.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + this._findNextWithEvent(xterm, this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } } override reveal(initialInput?: string): void { - const instance = this._terminalService.activeInstance; - if (instance && this.inputValue && this.inputValue !== '') { + const xterm = this._terminalService.activeInstance?.xterm; + if (xterm && this.inputValue && this.inputValue !== '') { // trigger highlight all matches - instance.xterm?.findPrevious(this.inputValue, { incremental: true, regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }).then(foundMatch => { + this._findPreviousWithEvent(xterm, this.inputValue, { incremental: true, regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }).then(foundMatch => { this.updateButtons(foundMatch); + this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); }); } this.updateButtons(false); @@ -109,9 +112,9 @@ export class TerminalFindWidget extends SimpleFindWidget { protected _onInputChanged() { // Ignore input changes for now - const instance = this._terminalService.activeInstance; - if (instance?.xterm) { - instance.xterm.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true }).then(foundMatch => { + const xterm = this._terminalService.activeInstance?.xterm; + if (xterm) { + this._findPreviousWithEvent(xterm, this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: true }).then(foundMatch => { this.updateButtons(foundMatch); }); } @@ -130,6 +133,7 @@ export class TerminalFindWidget extends SimpleFindWidget { const instance = this._terminalService.activeInstance; if (instance) { instance.notifyFindWidgetFocusChanged(false); + instance.xterm?.clearActiveSearchDecoration(); } this._findWidgetFocused.reset(); } @@ -148,7 +152,24 @@ export class TerminalFindWidget extends SimpleFindWidget { if (instance.hasSelection()) { instance.clearSelection(); } - instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + const xterm = instance.xterm; + if (xterm) { + this._findPreviousWithEvent(xterm, this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + } } } + + private async _findNextWithEvent(xterm: IXtermTerminal, term: string, options: ISearchOptions): Promise { + return xterm.findNext(term, options).then(foundMatch => { + this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); + return foundMatch; + }); + } + + private async _findPreviousWithEvent(xterm: IXtermTerminal, term: string, options: ISearchOptions): Promise { + return xterm.findPrevious(term, options).then(foundMatch => { + this._register(Event.once(xterm.onDidChangeSelection)(() => xterm.clearActiveSearchDecoration())); + return foundMatch; + }); + } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 357c121317f..20847bea41f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -121,12 +121,14 @@ class SplitPaneContainer extends Disposable { } } - getRelativePaneSize(instance: ITerminalInstance): number { + getPaneSize(instance: ITerminalInstance): number { const paneForInstance = this._terminalToPane.get(instance); if (!paneForInstance) { return 0; } - return ((this.orientation === Orientation.HORIZONTAL ? paneForInstance.element.clientWidth : paneForInstance.element.clientHeight) / (this.orientation === Orientation.HORIZONTAL ? this._width : this._height)); + + const index = this._children.indexOf(paneForInstance); + return this._splitView.getViewSize(index); } private _addChild(instance: ITerminalInstance, index: number): void { @@ -337,12 +339,13 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById { const instances = this.terminalInstances.filter(instance => typeof instance.persistentProcessId === 'number' && instance.shouldPersist); + const totalSize = instances.map(t => this._splitPaneContainer?.getPaneSize(t) || 0).reduce((total, size) => total += size, 0); return { isActive: isActive, activePersistentProcessId: this.activeInstance ? this.activeInstance.persistentProcessId : undefined, terminals: instances.map(t => { return { - relativeSize: this._splitPaneContainer?.getRelativePaneSize(t) || 0, + relativeSize: totalSize > 0 ? this._splitPaneContainer!.getPaneSize(t) / totalSize : 0, terminal: t.persistentProcessId || 0 }; }) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 88b3e2d041d..61c2efbb885 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -48,7 +48,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; @@ -80,6 +80,7 @@ import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import type { ITerminalAddon, Terminal as XTermTerminal } from 'xterm'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const enum Constants { /** @@ -361,7 +362,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IEditorService private readonly _editorService: IEditorService, @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IHistoryService private readonly _historyService: IHistoryService + @IHistoryService private readonly _historyService: IHistoryService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); @@ -1579,7 +1581,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { * @param exitCode The exit code of the process, this is undefined when the terminal was exited * through user action. */ - private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError, shellIntegrationAttempted?: boolean): Promise { + private async _onProcessExit(exitCodeOrError?: number | ITerminalLaunchError, failedShellIntegrationInjection?: boolean): Promise { // Prevent dispose functions being triggered multiple times if (this._isExiting) { return; @@ -1590,7 +1592,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { await this._flushXtermData(); this._logService.debug(`Terminal process exit (instanceId: ${this.instanceId}) with code ${this._exitCode}`); - const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd, shellIntegrationAttempted); + const parsedExitResult = parseExitResult(exitCodeOrError, this.shellLaunchConfig, this._processManager.processState, this._initialCwd, failedShellIntegrationInjection); this._exitCode = parsedExitResult?.code; const exitMessage = parsedExitResult?.message; @@ -1631,6 +1633,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } + if (failedShellIntegrationInjection) { + this._telemetryService.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationFailureProcessExit'); + } + // First onExit to consumers, this can happen after the terminal has already been disposed. this._onExit.fire(exitCodeOrError); @@ -2577,7 +2583,7 @@ export function parseExitResult( shellLaunchConfig: IShellLaunchConfig, processState: ProcessState, initialCwd: string | undefined, - shellIntegrationAttempted?: boolean + failedShellIntegrationInjection?: boolean ): { code: number | undefined; message: string | undefined } | undefined { // Only return a message if the exit code is non-zero if (exitCodeOrError === undefined || exitCodeOrError === 0) { @@ -2599,7 +2605,7 @@ export function parseExitResult( commandLine += shellLaunchConfig.args.map(a => ` '${a}'`).join(); } } - if (shellIntegrationAttempted) { + if (failedShellIntegrationInjection) { if (commandLine) { message = nls.localize('launchFailed.exitCodeAndCommandLineShellIntegration', "The terminal process \"{0}\" failed to launch (exit code: {1}). Disabling shell integration with `terminal.integrated.shellIntegration.enabled` might help.", commandLine, code); } else { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 2a5080e8115..7ae7dc3c4fc 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -33,6 +33,7 @@ import { NaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabil import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { URI } from 'vs/base/common/uri'; import { ISerializedCommandDetectionCapability } from 'vs/platform/terminal/common/terminalProcess'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -130,7 +131,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService + @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); @@ -332,6 +334,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce case ProcessPropertyType.HasChildProcesses: this._hasChildProcesses = value; break; + case ProcessPropertyType.FailedShellIntegrationActivation: + this._telemetryService?.publicLog2<{ classification: 'SystemMetaData'; purpose: 'FeatureInsight' }>('terminal/shellIntegrationActivationFailureCustomArgs'); + break; } this._onDidChangeProperty.fire({ type, value }); }) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 868644fc918..0a39093d05e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -40,7 +40,7 @@ import { once } from 'vs/base/common/functional'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; +import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; import { terminalStrings } from 'vs/workbench/contrib/terminal/common/terminalStrings'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index 27c0a94e424..509db6cc5a6 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -34,6 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { DecorationAddon } from 'vs/workbench/contrib/terminal/browser/xterm/decorationAddon'; import { ITerminalCapabilityStore, ITerminalCommand } from 'vs/platform/terminal/common/capabilities/capabilities'; import { Emitter } from 'vs/base/common/event'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -73,9 +74,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { private readonly _onDidRequestRunCommand = new Emitter<{ command: ITerminalCommand; copyAsHtml?: boolean }>(); readonly onDidRequestRunCommand = this._onDidRequestRunCommand.event; - private readonly _onDidChangeFindResults = new Emitter<{ resultIndex: number; resultCount: number } | undefined>(); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; + private readonly _onDidChangeSelection = new Emitter(); + readonly onDidChangeSelection = this._onDidChangeSelection.event; get commandTracker(): ICommandTracker { return this._commandNavigationAddon; } get shellIntegration(): IShellIntegration { return this._shellIntegrationAddon; } @@ -103,7 +105,8 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { @INotificationService private readonly _notificationService: INotificationService, @IStorageService private readonly _storageService: IStorageService, @IThemeService private readonly _themeService: IThemeService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); this.target = location; @@ -153,7 +156,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } if (e.affectsConfiguration(TerminalSettingId.ShellIntegrationDecorationsEnabled) || e.affectsConfiguration(TerminalSettingId.ShellIntegrationEnabled)) { - this._updateDecorationAddon(); + this._updateShellIntegrationAddons(); } })); @@ -165,14 +168,18 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } })); + // Refire events + this.add(this.raw.onSelectionChange(() => this._onDidChangeSelection.fire())); + // Load addons this._updateUnicodeVersion(); this._commandNavigationAddon = this._instantiationService.createInstance(CommandNavigationAddon, _capabilities); this.raw.loadAddon(this._commandNavigationAddon); - this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon); + this._shellIntegrationAddon = this._instantiationService.createInstance(ShellIntegrationAddon, this._telemetryService); this.raw.loadAddon(this._shellIntegrationAddon); - this._updateDecorationAddon(); + this._updateShellIntegrationAddons(); } + private _createDecorationAddon(): void { this._decorationAddon = this._instantiationService.createInstance(DecorationAddon, this._capabilities); this._decorationAddon.onDidRequestRunCommand(e => this._onDidRequestRunCommand.fire(e)); @@ -287,6 +294,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { // The mapping is as follows: // - findMatch -> activeMatch // - findMatchHighlight -> match + const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR) || theme.getColor(PANEL_BACKGROUND); const findMatchBackground = theme.getColor(TERMINAL_FIND_MATCH_BACKGROUND_COLOR); const findMatchBorder = theme.getColor(TERMINAL_FIND_MATCH_BORDER_COLOR); const findMatchOverviewRuler = theme.getColor(TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR); @@ -294,10 +302,11 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { const findMatchHighlightBorder = theme.getColor(TERMINAL_FIND_MATCH_HIGHLIGHT_BORDER_COLOR); const findMatchHighlightOverviewRuler = theme.getColor(TERMINAL_OVERVIEW_RULER_FIND_MATCH_FOREGROUND_COLOR); searchOptions.decorations = { - activeMatchBackground: findMatchBackground?.toString() || 'transparent', + activeMatchBackground: findMatchBackground?.toString(), activeMatchBorder: findMatchBorder?.toString() || 'transparent', activeMatchColorOverviewRuler: findMatchOverviewRuler?.toString() || 'transparent', - matchBackground: findMatchHighlightBackground?.toString() || 'transparent', + // decoration bgs don't support the alpha channel so blend it with the regular bg + matchBackground: terminalBackground ? findMatchHighlightBackground?.blend(terminalBackground).toString() : undefined, matchBorder: findMatchHighlightBorder?.toString() || 'transparent', matchOverviewRuler: findMatchHighlightOverviewRuler?.toString() || 'transparent' }; @@ -321,6 +330,10 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { this._searchAddon?.clearDecorations(); } + clearActiveSearchDecoration(): void { + this._searchAddon?.clearActiveDecoration(); + } + getFont(): ITerminalFont { return this._configHelper.getFont(this._core); } @@ -595,12 +608,16 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal { } } - private _updateDecorationAddon(): void { - if (this._configHelper.config.shellIntegration?.enabled && this._configHelper.config.shellIntegration.decorationsEnabled) { - if (!this._decorationAddon) { + private _updateShellIntegrationAddons(): void { + const shellIntegrationEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled); + const decorationsEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationDecorationsEnabled); + if (shellIntegrationEnabled) { + if (decorationsEnabled && !this._decorationAddon) { this._createDecorationAddon(); + } else if (this._decorationAddon && !decorationsEnabled) { + this._decorationAddon.dispose(); + this._decorationAddon = undefined; } - return; } if (this._decorationAddon) { this._decorationAddon.dispose(); diff --git a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts index 59ba5731ce5..bdfbe2ad4d0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; -import { registerColor, ColorIdentifier, ColorDefaults, editorFindMatch, editorFindMatchHighlight, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, ColorIdentifier, ColorDefaults, editorFindMatch, editorFindMatchHighlight, overviewRulerFindMatchForeground, editorSelectionBackground } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_BORDER, TAB_ACTIVE_BORDER } from 'vs/workbench/common/theme'; /** @@ -24,10 +24,10 @@ export const TERMINAL_FOREGROUND_COLOR = registerColor('terminal.foreground', { export const TERMINAL_CURSOR_FOREGROUND_COLOR = registerColor('terminalCursor.foreground', null, nls.localize('terminalCursor.foreground', 'The foreground color of the terminal cursor.')); export const TERMINAL_CURSOR_BACKGROUND_COLOR = registerColor('terminalCursor.background', null, nls.localize('terminalCursor.background', 'The background color of the terminal cursor. Allows customizing the color of a character overlapped by a block cursor.')); export const TERMINAL_SELECTION_BACKGROUND_COLOR = registerColor('terminal.selectionBackground', { - light: '#00000040', - dark: '#FFFFFF40', - hcDark: '#FFFFFF80', - hcLight: '#0F4A85' + light: editorSelectionBackground, + dark: editorSelectionBackground, + hcDark: editorSelectionBackground, + hcLight: editorSelectionBackground }, nls.localize('terminal.selectionBackground', 'The selection background color of the terminal.')); export const TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR = registerColor('terminalCommandDecoration.defaultBackground', { light: '#00000040', @@ -60,26 +60,27 @@ export const TERMINAL_BORDER_COLOR = registerColor('terminal.border', { hcLight: PANEL_BORDER }, nls.localize('terminal.border', 'The color of the border that separates split panes within the terminal. This defaults to panel.border.')); export const TERMINAL_FIND_MATCH_BACKGROUND_COLOR = registerColor('terminal.findMatchBackground', { - dark: null, - light: null, - hcDark: null, - hcLight: null -}, nls.localize('terminal.findMatchBackground', 'Color of the current search match in the terminal. The color must not be opaque so as not to hide underlying terminal content.')); -export const TERMINAL_FIND_MATCH_BORDER_COLOR = registerColor('terminal.findMatchBorder', { dark: editorFindMatch, light: editorFindMatch, + // Use regular selection background in high contrast with a thick border + hcDark: null, + hcLight: '#0F4A85' +}, nls.localize('terminal.findMatchBackground', 'Color of the current search match in the terminal. The color must not be opaque so as not to hide underlying terminal content.')); +export const TERMINAL_FIND_MATCH_BORDER_COLOR = registerColor('terminal.findMatchBorder', { + dark: null, + light: null, hcDark: '#f38518', hcLight: '#0F4A85' }, nls.localize('terminal.findMatchBorder', 'Border color of the current search match in the terminal.')); export const TERMINAL_FIND_MATCH_HIGHLIGHT_BACKGROUND_COLOR = registerColor('terminal.findMatchHighlightBackground', { - dark: null, - light: null, + dark: editorFindMatchHighlight, + light: editorFindMatchHighlight, hcDark: null, hcLight: null }, nls.localize('terminal.findMatchHighlightBackground', 'Color of the other search matches in the terminal. The color must not be opaque so as not to hide underlying terminal content.')); export const TERMINAL_FIND_MATCH_HIGHLIGHT_BORDER_COLOR = registerColor('terminal.findMatchHighlightBorder', { - dark: editorFindMatchHighlight, - light: editorFindMatchHighlight, + dark: null, + light: null, hcDark: '#f38518', hcLight: '#0F4A85' }, nls.localize('terminal.findMatchHighlightBorder', 'Border color of the other search matches in the terminal.')); diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts index f840fbf52d1..323dd56a2c1 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localPty.ts @@ -24,7 +24,8 @@ export class LocalPty extends Disposable implements ITerminalChildProcess { shellType: undefined, hasChildProcesses: true, resolvedShellLaunchConfig: {}, - overrideDimensions: undefined + overrideDimensions: undefined, + failedShellIntegrationActivation: false }; private readonly _onProcessData = this._register(new Emitter()); readonly onProcessData = this._onProcessData.event; diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts index 949fb57e709..448f145fda2 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts @@ -34,39 +34,44 @@ suite('Workbench - TerminalUriLinkDetector', () => { await assertLinkHelper(text, expected, detector, type); } - test('LinkComputer cases', async () => { - await assertLink(TerminalBuiltinLinkType.Url, 'x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, '(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, '[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, '{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, '', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'http://foo.bar', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, '// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]); - await assertLink(TerminalBuiltinLinkType.Url, '// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]); - await assertLink(TerminalBuiltinLinkType.Url, '// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]); - await assertLink(TerminalBuiltinLinkType.Url, '', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]); - await assertLink(TerminalBuiltinLinkType.Url, '請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]); - await assertLink(TerminalBuiltinLinkType.Url, '(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]); - await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.LocalFile, 'x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]); - await assertLink(TerminalBuiltinLinkType.Url, '7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]); - await assertLink(TerminalBuiltinLinkType.Url, '2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]); - await assertLink(TerminalBuiltinLinkType.Url, 'aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]); - }); + const linkComputerCases: [TerminalBuiltinLinkType, string, (Pick & { range: [number, number][] })[]][] = [ + [TerminalBuiltinLinkType.Url, 'x = "http://foo.bar";', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'x = (http://foo.bar);', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'x = \'http://foo.bar\';', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'x = http://foo.bar ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'x = ;', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'x = {http://foo.bar};', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, '(see http://foo.bar)', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, '[see http://foo.bar]', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, '{see http://foo.bar}', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, '', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'http://foo.bar', [{ range: [[6, 1], [19, 1]], text: 'http://foo.bar' }]], + [TerminalBuiltinLinkType.Url, '// Click here to learn more. https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409', [{ range: [[30, 1], [7, 2]], text: 'https://go.microsoft.com/fwlink/?LinkID=513275&clcid=0x409' }]], + [TerminalBuiltinLinkType.Url, '// Click here to learn more. https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx', [{ range: [[30, 1], [28, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]], + [TerminalBuiltinLinkType.Url, '// https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js', [{ range: [[4, 1], [9, 2]], text: 'https://github.com/projectkudu/kudu/blob/master/Kudu.Core/Scripts/selectNodeVersion.js' }]], + [TerminalBuiltinLinkType.Url, '', [{ range: [[49, 1], [14, 2]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]], + [TerminalBuiltinLinkType.Url, 'For instructions, see https://go.microsoft.com/fwlink/?LinkId=166007.', [{ range: [[23, 1], [68, 1]], text: 'https://go.microsoft.com/fwlink/?LinkId=166007' }]], + [TerminalBuiltinLinkType.Url, 'For instructions, see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx.', [{ range: [[23, 1], [21, 2]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx' }]], + [TerminalBuiltinLinkType.Url, 'x = "https://en.wikipedia.org/wiki/Zürich";', [{ range: [[6, 1], [41, 1]], text: 'https://en.wikipedia.org/wiki/Zürich' }]], + [TerminalBuiltinLinkType.Url, '請參閱 http://go.microsoft.com/fwlink/?LinkId=761051。', [{ range: [[8, 1], [53, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]], + [TerminalBuiltinLinkType.Url, '(請參閱 http://go.microsoft.com/fwlink/?LinkId=761051)', [{ range: [[10, 1], [55, 1]], text: 'http://go.microsoft.com/fwlink/?LinkId=761051' }]], + [TerminalBuiltinLinkType.LocalFile, 'x = "file:///foo.bar";', [{ range: [[6, 1], [20, 1]], text: 'file:///foo.bar' }]], + [TerminalBuiltinLinkType.LocalFile, 'x = "file://c:/foo.bar";', [{ range: [[6, 1], [22, 1]], text: 'file://c:/foo.bar' }]], + [TerminalBuiltinLinkType.LocalFile, 'x = "file://shares/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shares/foo.bar' }]], + [TerminalBuiltinLinkType.LocalFile, 'x = "file://shäres/foo.bar";', [{ range: [[6, 1], [26, 1]], text: 'file://shäres/foo.bar' }]], + [TerminalBuiltinLinkType.Url, 'Some text, then http://www.bing.com.', [{ range: [[17, 1], [35, 1]], text: 'http://www.bing.com' }]], + [TerminalBuiltinLinkType.Url, 'let url = `http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items`;', [{ range: [[12, 1], [78, 1]], text: 'http://***/_api/web/lists/GetByTitle(\'Teambuildingaanvragen\')/items' }]], + [TerminalBuiltinLinkType.Url, '7. At this point, ServiceMain has been called. There is no functionality presently in ServiceMain, but you can consult the [MSDN documentation](https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx) to add functionality as desired!', [{ range: [[66, 2], [64, 3]], text: 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms687414(v=vs.85).aspx' }]], + [TerminalBuiltinLinkType.Url, 'let x = "http://[::1]:5000/connect/token"', [{ range: [[10, 1], [40, 1]], text: 'http://[::1]:5000/connect/token' }]], + [TerminalBuiltinLinkType.Url, '2. Navigate to **https://portal.azure.com**', [{ range: [[18, 1], [41, 1]], text: 'https://portal.azure.com' }]], + [TerminalBuiltinLinkType.Url, 'POST|https://portal.azure.com|2019-12-05|', [{ range: [[6, 1], [29, 1]], text: 'https://portal.azure.com' }]], + [TerminalBuiltinLinkType.Url, 'aa https://foo.bar/[this is foo site] aa', [{ range: [[5, 1], [38, 1]], text: 'https://foo.bar/[this is foo site]' }]] + ]; + for (const c of linkComputerCases) { + test('link computer case: `' + c[1] + '`', async () => { + await assertLink(c[0], c[1], c[2]); + }); + } test('should support multiple link results', async () => { await assertLink(TerminalBuiltinLinkType.Url, 'http://foo.bar http://bar.foo', [ diff --git a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts index dca7e901bce..d0956de5457 100644 --- a/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts +++ b/src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchyPeek.ts @@ -329,8 +329,8 @@ export class TypeHierarchyTreePeekWidget extends peekView.PeekViewWidget { } if (fullRange) { this._editor.revealRangeInCenter(fullRange, ScrollType.Immediate); - const ids = this._editor.deltaDecorations([], decorations); - this._previewDisposable.add(toDisposable(() => this._editor.deltaDecorations(ids, []))); + const decorationsCollection = this._editor.createDecorationsCollection(decorations); + this._previewDisposable.add(toDisposable(() => decorationsCollection.clear())); } this._previewDisposable.add(value); diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index d9239b64135..057e5e10d4d 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -29,6 +29,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private _html: string = ''; private _initialScrollProgress: number = 0; private _state: string | undefined = undefined; + private _repositionTimeout: any | undefined = undefined; private _extension: WebviewExtensionDescription | undefined; private _contentOptions: WebviewContentOptions; @@ -76,6 +77,8 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } this._firstLoadPendingMessages.clear(); + clearTimeout(this._repositionTimeout); + this._onDidDispose.fire(); super.dispose(); @@ -144,6 +147,17 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } public layoutWebviewOverElement(element: HTMLElement, dimension?: Dimension, clippingContainer?: HTMLElement) { + this.doLayoutWebviewOverElement(element, dimension, clippingContainer); + + // Temporary fix for https://github.com/microsoft/vscode/issues/110450 + // There is an animation that lasts about 200ms, update the webview positioning once this animation is complete. + clearTimeout(this._repositionTimeout); + this._repositionTimeout = setTimeout(() => { + this.doLayoutWebviewOverElement(element, dimension, clippingContainer); + }, 200); + } + + public doLayoutWebviewOverElement(element: HTMLElement, dimension?: Dimension, clippingContainer?: HTMLElement) { if (!this._container || !this._container.parentElement) { return; } diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 1cb9e2a9af9..7834080ed70 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -323,9 +323,14 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const filter: IFilter = { name: languageName, extensions: distinct(extensions).slice(0, 10).map(e => trim(e, '.')) }; - if (!matchingFilter && extensions.indexOf(ext || PLAINTEXT_EXTENSION /* https://github.com/microsoft/vscode/issues/115860 */) >= 0) { + if (!matchingFilter && extensions.includes(ext || PLAINTEXT_EXTENSION /* https://github.com/microsoft/vscode/issues/115860 */)) { matchingFilter = filter; + const trimmedExt = trim(ext || PLAINTEXT_EXTENSION, '.'); + if (!filter.extensions.includes(trimmedExt)) { + filter.extensions.push(trimmedExt); + } + return null; // first matching filter will be added to the top } 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/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 8438e2f3377..aa1eb99bc0e 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -21,7 +21,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { localize } from 'vs/nls'; import * as semver from 'vs/base/common/semver/semver'; -import { isString } from 'vs/base/common/types'; +import { isBoolean, isString } from 'vs/base/common/types'; import { getErrorMessage } from 'vs/base/common/errors'; import { ResourceMap } from 'vs/base/common/map'; import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; @@ -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 deprecated = extensionsControlManifest.deprecated ? extensionsControlManifest.deprecated[extension.id.toLowerCase()] : undefined; + if (deprecated && !isBoolean(deprecated)) { + const preReleaseExtensionId = deprecated.id; + this.logService.info(`Checking additional builtin extensions: '${extension.id}' is deprecated, instead using '${preReleaseExtensionId}'`); + result.push({ id: preReleaseExtensionId, preRelease: !!deprecated.preRelease }); } else { result.push(extension); } diff --git a/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts b/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts index a790241645c..6991120b691 100644 --- a/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts +++ b/src/vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader.ts @@ -17,6 +17,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { RemoteAuthorities } from 'vs/base/common/network'; +import { getRemoteServerRootPath } from 'vs/platform/remote/common/remoteHosts'; export const WEB_EXTENSION_RESOURCE_END_POINT = 'web-extension-resource'; @@ -49,6 +50,7 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi readonly _serviceBrand: undefined; + private readonly _webExtensionResourceEndPoint: string; private readonly _extensionGalleryResourceUrlTemplate: string | undefined; private readonly _extensionGalleryAuthority: string | undefined; @@ -59,6 +61,7 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi private readonly _environmentService: IEnvironmentService, private readonly _configurationService: IConfigurationService, ) { + this._webExtensionResourceEndPoint = `${getRemoteServerRootPath(_productService)}/${WEB_EXTENSION_RESOURCE_END_POINT}/`; if (_productService.extensionsGallery) { this._extensionGalleryResourceUrlTemplate = _productService.extensionsGallery.resourceUrlTemplate; this._extensionGalleryAuthority = this._extensionGalleryResourceUrlTemplate ? this._getExtensionGalleryAuthority(URI.parse(this._extensionGalleryResourceUrlTemplate)) : undefined; @@ -115,7 +118,7 @@ export abstract class AbstractExtensionResourceLoaderService implements IExtensi } protected _isWebExtensionResourceEndPoint(uri: URI): boolean { - return uri.path.startsWith(`/${WEB_EXTENSION_RESOURCE_END_POINT}/`); + return uri.path.startsWith(this._webExtensionResourceEndPoint); } } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 310cf6d7745..c562da5671f 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -42,6 +42,7 @@ export const allApiProposals = Object.freeze({ 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', 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/views/browser/treeViewsService.ts b/src/vs/workbench/services/views/browser/treeViewsService.ts index 988300aadee..93d426deed5 100644 --- a/src/vs/workbench/services/views/browser/treeViewsService.ts +++ b/src/vs/workbench/services/views/browser/treeViewsService.ts @@ -5,7 +5,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDataTransfer } from 'vs/editor/common/dnd'; +import { IDataTransfer } from 'vs/base/common/dataTransfer'; import { ITreeItem } from 'vs/workbench/common/views'; import { ITreeViewsService as ITreeViewsServiceCommon, TreeviewsService } from 'vs/workbench/services/views/common/treeViewsService'; 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..992aec9c53f 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -343,7 +343,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/vscode-dts/vscode.proposed.notebookEditor.d.ts b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts index 5ed457eedbd..fd1d91db119 100644 --- a/src/vscode-dts/vscode.proposed.notebookEditor.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookEditor.d.ts @@ -39,11 +39,23 @@ declare module 'vscode' { export interface NotebookEditor { /** * The document associated with this notebook editor. + * + * @deprecated Use {@linkcode NotebookEditor.notebook} instead. */ readonly document: NotebookDocument; /** - * The selections on this notebook editor. + * 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 }`; */ @@ -54,6 +66,11 @@ declare module 'vscode' { */ 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. * @@ -61,11 +78,6 @@ declare module 'vscode' { * @param revealType The scrolling strategy for revealing `range`. */ revealRange(range: NotebookRange, revealType?: NotebookEditorRevealType): void; - - /** - * The column in which this editor shows. - */ - readonly viewColumn?: ViewColumn; } /** @@ -180,6 +192,8 @@ declare module 'vscode' { /** * A short-hand for `openNotebookDocument(uri).then(document => showNotebookDocument(document, options))`. * + * @deprecated Will not be finalized. + * * @see {@link workspace.openNotebookDocument} * * @param uri The resource to open. diff --git a/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts b/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts index 6c954b5f76c..b41878fd319 100644 --- a/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts @@ -7,32 +7,34 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/106744 - // todo@API add NotebookEdit-type which handles all these cases? - // export class NotebookEdit { - // range: NotebookRange; - // newCells: NotebookCellData[]; - // newMetadata?: NotebookDocumentMetadata; - // constructor(range: NotebookRange, newCells: NotebookCellData) - // } - - // export class NotebookCellEdit { - // newMetadata?: NotebookCellMetadata; - // } - - // export interface WorkspaceEdit { - // set(uri: Uri, edits: TextEdit[] | NotebookEdit[]): void - // } - export interface WorkspaceEdit { - // todo@API add NotebookEdit-type which handles all these cases? replaceNotebookMetadata(uri: Uri, value: { [key: string]: any }): void; + + /** + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + */ replaceNotebookCells(uri: Uri, range: NotebookRange, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; + + /** + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + */ replaceNotebookCellMetadata(uri: Uri, index: number, cellMetadata: { [key: string]: any }, metadata?: WorkspaceEditEntryMetadata): void; } export interface NotebookEditorEdit { + /** + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + */ replaceMetadata(value: { [key: string]: any }): void; + + /** + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + */ replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + + /** + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + */ replaceCellMetadata(index: number, metadata: { [key: string]: any }): void; } @@ -44,10 +46,11 @@ declare module 'vscode' { * be used to make edits. Note that the edit-builder is only valid while the * callback executes. * + * @deprecated Please migrate to the new `notebookWorkspaceEdit` proposed API. + * * @param callback A function which can create edits using an {@link NotebookEditorEdit edit-builder}. * @return A promise that resolves with a value indicating if the edits could be applied. */ - // @jrieken REMOVE maybe edit(callback: (editBuilder: NotebookEditorEdit) => void): Thenable; } } diff --git a/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts b/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts new file mode 100644 index 00000000000..14965fbe054 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookWorkspaceEdit.d.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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/106744 + + /** + * A notebook edit represents edits that should be applied to the contents of a notebook. + */ + export class NotebookEdit { + + /** + * Utility to create a edit that replaces cells in a notebook. + * + * @param range The range of cells to replace + * @param newCells The new notebook cells. + */ + static replaceCells(range: NotebookRange, newCells: NotebookCellData[]): NotebookEdit; + + /** + * 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 create an edit that update a cell's metadata. + * + * @param index The index of the cell to update. + * @param newCellMetadata The new metadata for the cell. + */ + static updateCellMetadata(index: number, newCellMetadata: { [key: string]: any }): NotebookEdit; + + /** + * Utility to create an edit that updates the notebook's metadata. + * + * @param newNotebookMetadata The new metadata for the notebook. + */ + 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. + */ + newCells: NotebookCellData[]; + + /** + * Optional new metadata for the cells. + */ + newCellMetadata?: { [key: string]: any }; + + /** + * Optional new metadata for the notebook. + */ + newNotebookMetadata?: { [key: string]: any }; + + constructor(range: NotebookRange, newCells: NotebookCellData[]); + } + + export interface WorkspaceEdit { + /** + * Set (and replace) edits for a resource. + * + * @param uri A resource identifier. + * @param edits An array of text or notebook edits. + */ + set(uri: Uri, edits: TextEdit[] | NotebookEdit[]): void; + } +} 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/smoke/src/areas/terminal/terminal-shellIntegration.test.ts b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts index 57d9eedca7a..a5f5842b4c6 100644 --- a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts +++ b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts @@ -25,7 +25,9 @@ export function setup() { }); describe('Shell integration', function () { - (process.platform === 'win32' ? describe.skip : describe)('Decorations', function () { + // TODO: Fix on Linux, some distros use sh as the default shell in which case shell integration will fail + // TODO: Fix on macOS, not sure reason for failing https://github.com/microsoft/vscode/issues/149757 + describe.skip('Decorations', function () { describe('Should show default icons', function () { it('Placeholder', async () => { await terminal.createTerminal(); diff --git a/yarn.lock b/yarn.lock index 36cf507aa65..73a712b8d7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12207,10 +12207,10 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-search@0.9.0-beta.26: - version "0.9.0-beta.26" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.26.tgz#24259b892ce5cd8eff207e4e334dc06776356fe5" - integrity sha512-gOz6v9do7yBDP8e4zdpnDIi3DsyPdLA10lsJDucfMN4nJFM2PjJAsu1fbqq1pXdcu14fHIYzbsp9wIMiW524zQ== +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-serialize@0.7.0-beta.12: version "0.7.0-beta.12" @@ -12222,20 +12222,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.29: - version "0.12.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" - integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== +xterm-addon-webgl@0.12.0-beta.34: + version "0.12.0-beta.34" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.34.tgz#51cac31cc7a78377be5d481b624ee82948360de1" + integrity sha512-TTIwun+a45oDN54sHhdUxsEx6VflgF2p9YGqS5+gVzpvPrEqP6GoDr6XFCDsZcSqi0ZT2FNGAKWlh7XSxsKQQw== -xterm-headless@4.19.0-beta.29: - version "4.19.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.29.tgz#9151a1506ddcad3402ce456bbbc6af0828952742" - integrity sha512-wAPyWOp2whY9kT9NL7PMQtvR/A9UO1A4bhP0nOOhZxg9GDeCy5EvsuDn2x+dtsh4jK/L2SZxM6SPHLpNoZpbTQ== +xterm-headless@4.19.0-beta.43: + version "4.19.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.43.tgz#12fe4abe624265240a7de8a922bfc4fd28c5f92a" + integrity sha512-4T8TlWy5u+sS23aPtd8gBHJ0BVljbNQRPMFHzLigDNOMCwc4uWa9JsxYmKteKifcG5aMm11ALPUTxWZCgpATww== -xterm@4.19.0-beta.29: - version "4.19.0-beta.29" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.29.tgz#f0727ddbfe54f3c34a58e57ecbfcbb4d03a30386" - integrity sha512-ZlgrxgotcCB06W0Pk5ClHDkIDE62s1LebgehEsmaksJJtoOQJIxCVu1Kop4EnnPQzZxFaG7uYumfwe0tfd6uWA== +xterm@4.19.0-beta.43: + version "4.19.0-beta.43" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.43.tgz#f2113c8ce303d22c5cfad1a4a119b81c103285fa" + integrity sha512-eQ3fzkUApGdl4/rrhzK4OIdMb3+qO0c2iCZIMbeP9SqqDltZnhWncz+3lGa0tnxKizVoUV9kmGaP7orsQ/IavQ== y18n@^3.2.1: version "3.2.2"