diff --git a/.github/commands.json b/.github/commands.json index 7a1220f7d43..980b2906bf5 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -202,6 +202,19 @@ "addLabel": "*caused-by-extension", "comment": "It looks like this is caused by the Python extension. Please file it with the repository [here](https://github.com/microsoft/vscode-python). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines for more information.\n\nHappy Coding!" }, + { + "type": "comment", + "name": "extJupyter", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "close", + "addLabel": "*caused-by-extension", + "comment": "It looks like this is caused by the Jupyter extension. Please file it with the repository [here](https://github.com/microsoft/vscode-jupyter). Make sure to check their issue reporting template and provide them relevant information such as the extension version you're using. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines for more information.\n\nHappy Coding!" + }, { "type": "comment", "name": "extC", diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index a066c194d99..6924d9650af 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index bda1b25092d..c2b8f95e0e6 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 439d2f2a46b..2734f6b7892 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index ffa11badc9c..1aaf05a875d 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index 1ce6faed4b2..42b5f1b6335 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index 7336c429df1..5253fb73dc8 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 4a3c30065f7..ec3bbb5e101 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index 190da52ba32..b27d79fda4a 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index abfce92748b..dca0f5be6f6 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index 3475b3ae878..75b7af42393 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,7 +14,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml index f06ba0f4ea0..67b8ec94ced 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 9e9076c11fa..ebfa1cb3445 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index b4a4f355fb5..e5ea1a26b81 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: "microsoft/vscode-github-triage-actions" - ref: v40 + ref: v41 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 57fcaa53165..6c2752107bc 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: v40 + ref: v41 - name: Install Actions if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') run: npm install --production --prefix ./actions diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index e749a11c4be..a3c81e4cc6e 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -104,7 +104,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE label:candidate", + "value": "$REPOS $MILESTONE is:open label:candidate", "editable": true } ] \ No newline at end of file diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 402ef474e56..54ec5328eba 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -122,7 +122,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE $MINE is:issue is:open", + "value": "$REPOS $MILESTONE $MINE is:issue is:open -label:endgame-plan", "editable": true }, { @@ -179,6 +179,18 @@ "value": "$REPOS $MILESTONE -$MINE is:issue is:closed author:@me sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found", "editable": true }, + { + "kind": 1, + "language": "markdown", + "value": "## Issues filed from outside team", + "editable": true + }, + { + "kind": 2, + "language": "github-issues", + "value": "$REPOS $MILESTONE -$MINE is:issue is:closed sort:updated-asc label:bug -label:verified -label:on-testplan -label:*duplicate -label:invalid -label:*as-designed -label:error-telemetry -label:verification-steps-needed -label:verification-found -author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:btholt -author:chrisdias -author:chrmarti -author:Chuxel -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:eamodio -author:egamma -author:fiveisprime -author:gregvanl -author:isidorn -author:ItalyPaleAle -author:JacksonKearl -author:joaomoreno -author:jrieken -author:kieferrm -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:ornellaalt -author:orta -author:rebornix -author:RMacfarlane -author:roblourens -author:rzhao271 -author:sana-ajani -author:sandy081 -author:sbatten -author:stevencl -author:Tyriar -author:weinand", + "editable": true + }, { "kind": 1, "language": "markdown", diff --git a/.vscode/notebooks/verification.github-issues b/.vscode/notebooks/verification.github-issues index 67db0cb97d4..582859397ef 100644 --- a/.vscode/notebooks/verification.github-issues +++ b/.vscode/notebooks/verification.github-issues @@ -14,7 +14,7 @@ { "kind": 2, "language": "github-issues", - "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"October 2020\"", + "value": "$repos=repo:microsoft/vscode repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-remote-release repo:microsoft/vscode-js-debug repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-github-issue-notebooks \n$milestone=milestone:\"November 2020\"", "editable": true }, { diff --git a/.yarnrc b/.yarnrc index 264a5e3a3d6..7b0e7edfcbf 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://electronjs.org/headers" -target "9.3.5" +target "11.0.3" runtime "electron" diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index f4b36447f47..0d0db3acade 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -66,7 +66,7 @@ This project incorporates components from the projects listed below. The origina 59. vscode-codicons version 0.0.1 (https://github.com/microsoft/vscode-codicons) 60. vscode-logfile-highlighter version 2.8.0 (https://github.com/emilast/vscode-logfile-highlighter) 61. vscode-swift version 0.0.1 (https://github.com/owensd/vscode-swift) -62. Web Background Synchronization (https://github.com/WICG/BackgroundSync) +62. Web Background Synchronization (https://github.com/WICG/background-sync) %% atom/language-clojure NOTICES AND INFORMATION BEGIN HERE diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b2f9a13c2b7..cce8119d463 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -16,3 +16,8 @@ jobs: vmImage: macOS-latest steps: - template: build/azure-pipelines/darwin/continuous-build-darwin.yml + +trigger: + branches: + exclude: + - electron-11.x.y diff --git a/build/.cachesalt b/build/.cachesalt index a7a4d508274..661e359818e 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2020-11-30T16:21:34.566Z +2020-12-05T15:02:48.675Z diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index d7e62629cb8..daf60d710ee 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -11,6 +11,7 @@ import * as crypto from 'crypto'; import * as azure from 'azure-storage'; import * as mime from 'mime'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; interface Asset { platform: string; @@ -121,7 +122,7 @@ async function main(): Promise { const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); + await retry(() => scripts.storedProcedure('createAsset').execute('', [commit, asset, true])); } main().then(() => { diff --git a/build/azure-pipelines/common/createBuild.ts b/build/azure-pipelines/common/createBuild.ts index c8fd66b791d..e314d7c0988 100644 --- a/build/azure-pipelines/common/createBuild.ts +++ b/build/azure-pipelines/common/createBuild.ts @@ -6,6 +6,7 @@ 'use strict'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; if (process.argv.length !== 3) { console.error('Usage: node createBuild.js VERSION'); @@ -48,7 +49,7 @@ async function main(): Promise { const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, key: process.env['AZURE_DOCUMENTDB_MASTERKEY'] }); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }]); + await retry(() => scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }])); } main().then(() => { diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index ac49e7b0042..d42b3f1a078 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -6,6 +6,7 @@ 'use strict'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; function getEnv(name: string): string { const result = process.env[name]; @@ -58,7 +59,7 @@ async function main(): Promise { console.log(`Releasing build ${commit}...`); const scripts = client.database('builds').container(quality).scripts; - await scripts.storedProcedure('releaseBuild').execute('', [commit]); + await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); } main().then(() => { diff --git a/build/azure-pipelines/common/retry.ts b/build/azure-pipelines/common/retry.ts new file mode 100644 index 00000000000..1737676590d --- /dev/null +++ b/build/azure-pipelines/common/retry.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +export async function retry(fn: () => Promise): Promise { + for (let run = 1; run <= 10; run++) { + try { + return await fn(); + } catch (err) { + if (!/ECONNRESET/.test(err.message)) { + throw err; + } + + const millis = (Math.random() * 200) + (50 * Math.pow(1.5, run)); + console.log(`Failed with ECONNRESET, retrying in ${millis}ms...`); + + // maximum delay is 10th retry: ~3 seconds + await new Promise(c => setTimeout(c, millis)); + } + } + + throw new Error('Retried too many times'); +} diff --git a/build/azure-pipelines/common/sync-mooncake.ts b/build/azure-pipelines/common/sync-mooncake.ts index 76f12185e12..4ffe7a8f15b 100644 --- a/build/azure-pipelines/common/sync-mooncake.ts +++ b/build/azure-pipelines/common/sync-mooncake.ts @@ -9,6 +9,7 @@ import * as url from 'url'; import * as azure from 'azure-storage'; import * as mime from 'mime'; import { CosmosClient } from '@azure/cosmos'; +import { retry } from './retry'; function log(...args: any[]) { console.log(...[`[${new Date().toISOString()}]`, ...args]); @@ -99,8 +100,8 @@ async function sync(commit: string, quality: string): Promise { log(` Updating build in DB...`); const mooncakeUrl = `${process.env['MOONCAKE_CDN_URL']}${blobPath}`; - await container.scripts.storedProcedure('setAssetMooncakeUrl') - .execute('', [commit, asset.platform, asset.type, mooncakeUrl]); + await retry(() => container.scripts.storedProcedure('setAssetMooncakeUrl') + .execute('', [commit, asset.platform, asset.type, mooncakeUrl])); log(` Done ✔️`); } catch (err) { diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 55efe5f32eb..75c28106c5e 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist b/build/azure-pipelines/darwin/helper-plugin-entitlements.plist deleted file mode 100644 index 7cd9df032bd..00000000000 --- a/build/azure-pipelines/darwin/helper-plugin-entitlements.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.cs.allow-unsigned-executable-memory - - com.apple.security.cs.disable-library-validation - - - diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 6f7dec16aca..28da2b70ee0 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -22,7 +22,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -78,10 +78,9 @@ steps: - script: | set -e - npm install -g node-gyp@7.1.0 + npm install -g node-gyp@latest node-gyp --version displayName: Update node-gyp - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - script: | set -e diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 99bad6b72cd..331fbf9675e 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -8,7 +8,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index c20ff939ad9..e0bb30a7e02 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -1,17 +1,12 @@ pool: vmImage: "Ubuntu-16.04" -trigger: - branches: - include: ["master"] -pr: - branches: - include: ["master"] +trigger: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index e777f821d76..44cb9829472 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -10,7 +10,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index 9a54bc2bc8a..8108ff8193d 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -22,7 +22,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 1e88a0a3b53..6c9d24da74f 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -22,7 +22,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -71,6 +71,18 @@ steps: - script: | set -e + npm install -g node-gyp@latest + node-gyp --version + displayName: Update node-gyp + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + + - script: | + set -e + if [ -z "$CC" || -z "$CXX" ] + then + export CC=$(which gcc-5) + export CXX=$(which g++-5) + fi export npm_config_arch=$(NPM_ARCH) export CHILD_CONCURRENCY="1" for i in {1..3}; do # try 3 times, for Terrapin @@ -97,6 +109,16 @@ steps: displayName: Run postinstall scripts condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + - script: | + set -e + export CC=$(which gcc-4.8) + export CXX=$(which g++-4.8) + export npm_config_node_gyp=$(which node-gyp) + cd remote && rm -rf node_modules/ + yarn + displayName: Rebuild remote modules with gcc-4.8 + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + - script: | set -e node build/azure-pipelines/mixin @@ -112,12 +134,6 @@ steps: yarn gulp vscode-reh-web-linux-$(VSCODE_ARCH)-min-ci displayName: Build - - script: | - set -e - service xvfb start - displayName: Start xvfb - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | set -e DISPLAY=:10 ./scripts/test.sh --build --tfs "Unit Tests" @@ -137,6 +153,7 @@ steps: 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)" \ DISPLAY=:10 ./scripts/test-integration.sh --build --tfs "Integration Tests" @@ -154,6 +171,7 @@ steps: 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)" \ DISPLAY=:10 ./resources/server/test/test-remote-integration.sh diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index f08c7b3c3e6..e0f2d2c6e2a 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index ebe2fe6abef..2b3d87d119d 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -11,8 +11,9 @@ schedules: resources: containers: - container: vscode-x64 - image: vscodehub.azurecr.io/vscode-linux-build-agent:x64 + image: vscodehub.azurecr.io/vscode-linux-build-agent:bionic-x64 endpoint: VSCodeHub + options: --user 0:0 - container: vscode-arm64 image: vscodehub.azurecr.io/vscode-linux-build-agent:stretch-arm64 endpoint: VSCodeHub @@ -27,7 +28,7 @@ stages: jobs: - job: Compile pool: - vmImage: "Ubuntu-16.04" + vmImage: "Ubuntu-18.04" container: vscode-x64 variables: VSCODE_ARCH: x64 @@ -70,7 +71,7 @@ stages: - Compile condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) pool: - vmImage: "Ubuntu-16.04" + vmImage: "Ubuntu-18.04" jobs: - job: Linux condition: and(succeeded(), eq(variables['VSCODE_BUILD_LINUX'], 'true')) @@ -179,7 +180,7 @@ stages: - macOSARM64 condition: and(succeededOrFailed(), eq(variables['VSCODE_COMPILE_ONLY'], 'false')) pool: - vmImage: "Ubuntu-16.04" + vmImage: "Ubuntu-18.04" jobs: - job: SyncMooncake displayName: Sync Mooncake @@ -194,7 +195,7 @@ stages: - macOSARM64 condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), or(eq(variables['VSCODE_RELEASE'], 'true'), and(or(eq(variables['VSCODE_QUALITY'], 'insider'), eq(variables['VSCODE_QUALITY'], 'exploration')), eq(variables['Build.Reason'], 'Schedule')))) pool: - vmImage: "Ubuntu-16.04" + vmImage: "Ubuntu-18.04" jobs: - job: BuildService displayName: Build Service diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index eaf0a65e8aa..66baef772c8 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -17,7 +17,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true')) - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 5f1cf6c28a4..0e3f4e4daa4 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -9,7 +9,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml index 109709418ff..280c9e6372d 100644 --- a/build/azure-pipelines/sync-mooncake.yml +++ b/build/azure-pipelines/sync-mooncake.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 33282adbe46..1beae9506eb 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -22,7 +22,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 87b820cb009..7bd18707381 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 1992ac1affb..5c8806401d5 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -22,7 +22,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.14.1" + versionSpec: "12.18.3" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 538dc97adf1..f1908b14749 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -29,7 +29,6 @@ async function main(): Promise { const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; - const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; const defaultOpts: codesign.SignOptions = { @@ -51,7 +50,6 @@ async function main(): Promise { // TODO(deepak1556): Incorrectly declared type in electron-osx-sign ignore: (filePath: string) => { return filePath.includes(gpuHelperAppName) || - filePath.includes(pluginHelperAppName) || filePath.includes(rendererHelperAppName); } }; @@ -63,13 +61,6 @@ async function main(): Promise { 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), }; - const pluginHelperOpts: codesign.SignOptions = { - ...defaultOpts, - app: path.join(appFrameworkPath, pluginHelperAppName), - entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), - }; - const rendererHelperOpts: codesign.SignOptions = { ...defaultOpts, app: path.join(appFrameworkPath, rendererHelperAppName), @@ -78,7 +69,6 @@ async function main(): Promise { }; await codesign.signAsync(gpuHelperOpts); - await codesign.signAsync(pluginHelperOpts); await codesign.signAsync(rendererHelperOpts); await codesign.signAsync(appOpts as any); } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 6e556444fe1..bb8b2454a9b 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -58,7 +58,7 @@ const vscodeResources = [ 'out-build/bootstrap-node.js', 'out-build/bootstrap-window.js', 'out-build/paths.js', - 'out-build/vs/**/*.{svg,png,html}', + 'out-build/vs/**/*.{svg,png,html,jpg}', '!out-build/vs/code/browser/**/*.html', '!out-build/vs/editor/standalone/**/*.svg', 'out-build/vs/base/common/performance.js', @@ -80,7 +80,6 @@ const vscodeResources = [ 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-sandbox/issue/issueReporter.js', 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js', - 'out-build/vs/code/electron-sandbox/proxy/auth.js', '!**/test/**' ]; @@ -201,7 +200,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const telemetry = gulp.src('.build/telemetry/**', { base: '.build/telemetry', dot: true }); - const jsFilter = util.filter(data => !data.isDirectory() &&/\.js$/.test(data.path)); + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); const root = path.resolve(path.join(__dirname, '..')); const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(root, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`])); @@ -338,7 +337,7 @@ BUILD_TARGETS.forEach(buildTarget => { const arch = buildTarget.arch; const opts = buildTarget.opts; - ['', 'min'].forEach(minified => { + const [vscode, vscodeMin] = ['', 'min'].map(minified => { const sourceFolderName = `out-vscode${dashed(minified)}`; const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; @@ -355,7 +354,14 @@ BUILD_TARGETS.forEach(buildTarget => { vscodeTaskCI )); gulp.task(vscodeTask); + + return vscodeTask; }); + + if (process.platform === platform && process.arch === arch) { + gulp.task(task.define('vscode', task.series(vscode))); + gulp.task(task.define('vscode-min', task.series(vscodeMin))); + } }); // Transifex Localizations diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 2833bb14a20..9de7f9bb62c 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -98,6 +98,7 @@ function prepareDebPackage(arch) { .pipe(replace('@@ARCHITECTURE@@', debArch)) .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) + .pipe(replace('@@REPOSITORY_NAME@@', arch === 'x64' ? 'vscode' : 'code')) .pipe(rename('DEBIAN/postinst')); const all = es.merge(control, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 1c7579206af..c0d67db6017 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -25,8 +25,6 @@ import { match } from 'minimatch'; // Feel free to add more core types as you see needed if present in node.js and browsers const CORE_TYPES = [ 'require', // from our AMD loader - // 'atob', - // 'btoa', 'setTimeout', 'clearTimeout', 'setInterval', diff --git a/cgmanifest.json b/cgmanifest.json index 656e3f821bf..5575cb4b04c 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "894fb9eb56c6cbda65e3c3ae9ada6d4cb5850cc9" + "commitHash": "0387c513f0319231a8ca6dd0a265102af3d4455e" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "83.0.4103.122" + "version": "87.0.4280.67" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "9622fed3fb2cffcea9efff6c8cb4cc2def99d75d" + "commitHash": "e3e0927bb93ed92bcdfe81e7ad9af3d78ccc74fb" } }, "isOnlyProductionDependency": true, - "version": "12.14.1" + "version": "12.18.3" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "415c1f9e9b35d9599b1a8ad1200476afa47a3323" + "commitHash": "b0862a6e63173c4c919bd5ed27d257235bbfe7d2" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "9.3.5" + "version": "11.0.3" }, { "component": { diff --git a/extensions/configuration-editing/src/configurationEditingMain.ts b/extensions/configuration-editing/src/configurationEditingMain.ts index 966073e23f8..a3ef34f3d83 100644 --- a/extensions/configuration-editing/src/configurationEditingMain.ts +++ b/extensions/configuration-editing/src/configurationEditingMain.ts @@ -81,7 +81,7 @@ function registerExtensionsCompletionsInExtensionsDocument(): vscode.Disposable const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'recommendations') { const extensionsContent = parse(document.getText()); - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } @@ -95,7 +95,7 @@ function registerExtensionsCompletionsInWorkspaceConfigurationDocument(): vscode const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); if (location.path[0] === 'extensions' && location.path[1] === 'recommendations') { const extensionsContent = parse(document.getText())['extensions']; - return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], range, false); + return provideInstalledExtensionProposals(extensionsContent && extensionsContent.recommendations || [], '', range, false); } return []; } diff --git a/extensions/configuration-editing/src/extensionsProposals.ts b/extensions/configuration-editing/src/extensionsProposals.ts index 60533ae2975..7fef9836bdc 100644 --- a/extensions/configuration-editing/src/extensionsProposals.ts +++ b/extensions/configuration-editing/src/extensionsProposals.ts @@ -8,14 +8,14 @@ import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -export function provideInstalledExtensionProposals(existing: string[], range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { +export function provideInstalledExtensionProposals(existing: string[], additionalText: string, range: vscode.Range, includeBuiltinExtensions: boolean): vscode.ProviderResult { if (Array.isArray(existing)) { const extensions = includeBuiltinExtensions ? vscode.extensions.all : vscode.extensions.all.filter(e => !(e.id.startsWith('vscode.') || e.id === 'Microsoft.vscode-markdown')); const knownExtensionProposals = extensions.filter(e => existing.indexOf(e.id) === -1); if (knownExtensionProposals.length) { return knownExtensionProposals.map(e => { const item = new vscode.CompletionItem(e.id); - const insertText = `"${e.id}"`; + const insertText = `"${e.id}"${additionalText}`; item.kind = vscode.CompletionItemKind.Value; item.insertText = insertText; item.range = range; diff --git a/extensions/configuration-editing/src/settingsDocumentHelper.ts b/extensions/configuration-editing/src/settingsDocumentHelper.ts index 5e466c2eb6f..f3461f0c3b5 100644 --- a/extensions/configuration-editing/src/settingsDocumentHelper.ts +++ b/extensions/configuration-editing/src/settingsDocumentHelper.ts @@ -48,7 +48,16 @@ export class SettingsDocument { try { ignoredExtensions = parse(this.document.getText())['settingsSync.ignoredExtensions']; } catch (e) {/* ignore error */ } - return provideInstalledExtensionProposals(ignoredExtensions, range, true); + return provideInstalledExtensionProposals(ignoredExtensions, '', range, true); + } + + // remote.extensionKind + if (location.path[0] === 'remote.extensionKind' && location.path.length === 2 && location.isAtPropertyKey) { + let alreadyConfigured: string[] = []; + try { + alreadyConfigured = Object.keys(parse(this.document.getText())['remote.extensionKind']); + } catch (e) {/* ignore error */ } + return provideInstalledExtensionProposals(alreadyConfigured, `: [\n\t"ui"\n]`, range, true); } return this.provideLanguageOverridesCompletionItems(location, position); diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index 440b3296e57..b47641f2db2 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -156,7 +156,7 @@ async function toggleAutoAttachSetting(context: vscode.ExtensionContext, scope?: quickPick.show(); - const result = await new Promise(resolve => { + let result = await new Promise(resolve => { quickPick.onDidAccept(() => resolve(quickPick.selectedItems[0])); quickPick.onDidHide(() => resolve(undefined)); quickPick.onDidTriggerButton(() => { @@ -179,7 +179,11 @@ async function toggleAutoAttachSetting(context: vscode.ExtensionContext, scope?: } if ('state' in result) { - section.update(SETTING_STATE, result.state, scope); + if (result.state !== current) { + section.update(SETTING_STATE, result.state, scope); + } else if (isTemporarilyDisabled) { + result = { setTempDisabled: false }; + } } if ('setTempDisabled' in result) { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index f3b830a6b04..2c24d9095b4 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -54,7 +54,7 @@ class CheckoutRemoteHeadItem extends CheckoutItem { return localize('remote branch at', "Remote branch at {0}", this.shortCommit); } - async run(repository: Repository): Promise { + async run(repository: Repository, opts?: { detached?: boolean }): Promise { if (!this.ref.name) { return; } @@ -62,9 +62,9 @@ class CheckoutRemoteHeadItem extends CheckoutItem { const branches = await repository.findTrackingBranches(this.ref.name); if (branches.length > 0) { - await repository.checkout(branches[0].name!); + await repository.checkout(branches[0].name!, opts); } else { - await repository.checkoutTracking(this.ref.name); + await repository.checkoutTracking(this.ref.name, opts); } } } diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index f7948563a01..87f24395d6d 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1264,8 +1264,8 @@ export class Repository implements Disposable { await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [], opts)); } - async checkoutTracking(treeish: string): Promise { - await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { track: true })); + async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise { + await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { ...opts, track: true })); } async findTrackingBranches(upstreamRef: string): Promise { diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 3e7a0dbd62d..c2c211ccbaa 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -144,7 +144,7 @@ "null" ], "scope": "resource", - "default": "null", + "default": null, "description": "%html.format.wrapAttributesIndentSize.desc%" }, "html.format.templating": { diff --git a/extensions/make/language-configuration.json b/extensions/make/language-configuration.json index f3c19d01120..82645b84dfb 100644 --- a/extensions/make/language-configuration.json +++ b/extensions/make/language-configuration.json @@ -17,6 +17,18 @@ ] ], "autoClosingPairs": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], { "open": "'", "close": "'", diff --git a/extensions/markdown-language-features/src/features/smartSelect.ts b/extensions/markdown-language-features/src/features/smartSelect.ts index 0fca6a5483c..12b2bdd78b3 100644 --- a/extensions/markdown-language-features/src/features/smartSelect.ts +++ b/extensions/markdown-language-features/src/features/smartSelect.ts @@ -34,7 +34,7 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide const tokens = await this.engine.parse(document); - const blockTokens = getBlockTokensForPosition(tokens, position); + const blockTokens = getBlockTokensForPosition(tokens, position, headerRange); if (blockTokens.length === 0) { return undefined; @@ -91,13 +91,13 @@ function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, // of this header then all content then header return new vscode.SelectionRange(contentRange.with(undefined, startOfChildRange), new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(range, parent)))); } else { - // no children and not on this header line so select content then header + // not on this header line so select content then header return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); } } -function getBlockTokensForPosition(tokens: Token[], position: vscode.Position): Token[] { - const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && isBlockElement(token)); +function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): Token[] { + const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token)); if (enclosingTokens.length === 0) { return []; } @@ -131,9 +131,17 @@ function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode const lineText = document.lineAt(cursorPosition.line).text; const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent); const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent); - const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, boldSelection ? boldSelection : italicSelection || parent); + let comboSelection: vscode.SelectionRange | undefined; + if (boldSelection && italicSelection && !boldSelection.range.isEqual(italicSelection.range)) { + if (boldSelection.range.contains(italicSelection.range)) { + comboSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, boldSelection); + } else if (italicSelection.range.contains(boldSelection.range)) { + comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection); + } + } + const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection || boldSelection || italicSelection || parent); const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection || parent); - return inlineCodeBlockSelection || linkSelection || boldSelection || italicSelection; + return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection; } function createFencedRange(token: Token, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange { @@ -154,61 +162,31 @@ function createFencedRange(token: Token, cursorLine: number, document: vscode.Te } function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { - // find closest ** that occurs before cursor position - let startBold = lineText.substring(0, cursorChar).lastIndexOf('**'); - - // find closest ** that occurs after the start ** - const endBoldIndex = lineText.substring(startBold + 2).indexOf('**'); - let endBold = startBold + 2 + lineText.substring(startBold + 2).indexOf('**'); - - if (startBold >= 0 && endBoldIndex >= 0 && startBold + 1 < endBold && startBold <= cursorChar && endBold >= cursorChar) { - const range = new vscode.Range(cursorLine, startBold, cursorLine, endBold + 2); - // **content cursor content** so select content then ** on both sides - const contentRange = new vscode.Range(cursorLine, startBold + 2, cursorLine, endBold); - return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); - } else if (startBold >= 0) { - // **content**cursor or **content*cursor* - // find end ** from end of start ** to end of line (since the cursor is within the end stars) - let adjustedEnd = startBold + 2 + lineText.substring(startBold + 2).indexOf('**'); - startBold = lineText.substring(0, adjustedEnd - 2).lastIndexOf('**'); - if (adjustedEnd >= 0 && cursorChar === adjustedEnd || cursorChar === adjustedEnd + 1) { - if (lineText.charAt(adjustedEnd + 1) === '*') { - // *cursor* so need to extend end to include the second * - adjustedEnd += 1; - } - return new vscode.SelectionRange(new vscode.Range(cursorLine, startBold, cursorLine, adjustedEnd + 1), parent); - } - } else if (endBold > 0) { - // cursor**content** or *cursor*content** - // find start ** from start of string to cursor + 2 (since the cursor is within the start stars) - const adjustedStart = lineText.substring(0, cursorChar + 2).lastIndexOf('**'); - endBold = adjustedStart + 2 + lineText.substring(adjustedStart + 2).indexOf('**'); - if (adjustedStart >= 0 && adjustedStart === cursorChar || adjustedStart === cursorChar - 1) { - return new vscode.SelectionRange(new vscode.Range(cursorLine, adjustedStart, cursorLine, endBold + 2), parent); - } + const regex = /(?:^|(?<=\s))(?:\*\*\s*([^*]+)(?:\*\s*([^*]+)\s*?\*)*([^*]+)\s*?\*\*)/g; + const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); + if (matches.length > 0) { + // should only be one match, so select first and index 0 contains the entire match + const bold = matches[0][0]; + const startIndex = lineText.indexOf(bold); + const cursorOnStars = cursorChar === startIndex || cursorChar === startIndex + 1 || cursorChar === startIndex + bold.length || cursorChar === startIndex + bold.length - 1; + const contentAndStars = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + bold.length), parent); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 2, cursorLine, startIndex + bold.length - 2), contentAndStars); + return cursorOnStars ? contentAndStars : content; } return undefined; } function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { - const type = isItalic ? '*' : '`'; - const start = lineText.substring(0, cursorChar + 1).lastIndexOf(type); - let end = lineText.substring(cursorChar).indexOf(type); - - if (start >= 0 && end >= 0) { - end += cursorChar; - // ensure there's no * or ` before end - const intermediate = lineText.substring(start + 1, end - 1).indexOf(type); - if (intermediate < 0) { - const range = new vscode.Range(cursorLine, start, cursorLine, end + 1); - if (cursorChar > start && cursorChar <= end) { - // within the content so select content then include the stars or backticks - const contentRange = new vscode.Range(cursorLine, start + 1, cursorLine, end); - return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent)); - } else if (cursorChar === start) { - return new vscode.SelectionRange(range, parent); - } - } + const regex = isItalic ? /(?:^|(?<=\s))(?:\*\s*([^*]+)(?:\*\*\s*([^*]+)\s*?\*\*)*([^*]+)\s*?\*)/g : /\`[^\`]*\`/g; + const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar); + if (matches.length > 0) { + // should only be one match, so select first and index 0 contains the entire match + const match = matches[0][0]; + const startIndex = lineText.indexOf(match); + const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length; + const contentAndType = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + match.length), parent); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 1, cursorLine, startIndex + match.length - 1), contentAndType); + return cursorOnType ? contentAndType : content; } return undefined; } @@ -228,11 +206,12 @@ function createLinkRange(lineText: string, cursorChar: number, cursorLine: numbe // determine if cursor is within [text] or (url) in order to know which should be selected const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url; + const indexOfType = lineText.indexOf(nearestType); // determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range - const cursorOnType = cursorChar === lineText.indexOf(nearestType) || cursorChar === lineText.indexOf(nearestType) + nearestType.length; + const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length; - const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType), cursorLine, lineText.indexOf(nearestType) + nearestType.length), linkRange); - const content = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType) + 1, cursorLine, lineText.indexOf(nearestType) + nearestType.length - 1), contentAndNearestType); + const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange); + const content = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType); return cursorOnType ? contentAndNearestType : content; } return undefined; diff --git a/extensions/markdown-language-features/src/test/smartSelect.test.ts b/extensions/markdown-language-features/src/test/smartSelect.test.ts index 98f2a5de607..6e77d56c23c 100644 --- a/extensions/markdown-language-features/src/test/smartSelect.test.ts +++ b/extensions/markdown-language-features/src/test/smartSelect.test.ts @@ -520,10 +520,10 @@ suite('markdown.SmartSelect', () => { `paragraph`, `## sub header`, `- list`, - `- stuff here [text]**${CURSOR}items in here** and **here**`, + `- stuff here [text] **${CURSOR}items in here** and **here**`, `- list` )); - assertNestedRangesEqual(ranges![0], [6, 21, 6, 44], [6, 19, 6, 46], [6, 0, 6, 59], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); + assertNestedRangesEqual(ranges![0], [6, 22, 6, 45], [6, 20, 6, 47], [6, 0, 6, 60], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]); }); test('Smart select link in paragraph with multiple links', async () => { const ranges = await getSelectionRangesForDocument( @@ -567,11 +567,69 @@ suite('markdown.SmartSelect', () => { )); assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 22], [0, 1, 0, 42], [0, 1, 0, 42], [0, 0, 0, 43], [0, 0, 0, 43]); }); + test('Smart select italic on end', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `*word1 word2 word3${CURSOR}*` + )); + assertNestedRangesEqual(ranges![0], [0, 1, 0, 28], [0, 0, 0, 29], [0, 0, 0, 29]); + }); + test('Smart select italic then bold', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `outer text **bold words *italic ${CURSOR} words* bold words** outer text` + )); + assertNestedRangesEqual(ranges![0], [0, 25, 0, 48], [0, 24, 0, 49], [0, 13, 0, 60], [0, 11, 0, 62], [0, 0, 0, 73]); + }); + test('Smart select bold then italic', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `outer text *italic words **bold ${CURSOR} words** italic words* outer text` + )); + assertNestedRangesEqual(ranges![0], [0, 27, 0, 48], [0, 25, 0, 50], [0, 12, 0, 63], [0, 11, 0, 64], [0, 0, 0, 75]); + }); + test('Third level header from release notes', async () => { + const ranges = await getSelectionRangesForDocument( + joinLines( + `---`, + `Order: 60`, + `TOCTitle: October 2020`, + `PageTitle: Visual Studio Code October 2020`, + `MetaDescription: Learn what is new in the Visual Studio Code October 2020 Release (1.51)`, + `MetaSocialImage: 1_51/release-highlights.png`, + `Date: 2020-11-6`, + `DownloadVersion: 1.51.1`, + `---`, + `# October 2020 (version 1.51)`, + ``, + `**Update 1.51.1**: The update addresses these [issues](https://github.com/microsoft/vscode/issues?q=is%3Aissue+milestone%3A%22October+2020+Recovery%22+is%3Aclosed+).`, + ``, + ``, + ``, + `---`, + ``, + `Welcome to the October 2020 release of Visual Studio Code. As announced in the [October iteration plan](https://github.com/microsoft/vscode/issues/108473), we focused on housekeeping GitHub issues and pull requests as documented in our issue grooming guide.`, + ``, + `We also worked with our partners at GitHub on GitHub Codespaces, which ended up being more involved than originally anticipated. To that end, we'll continue working on housekeeping for part of the November iteration.`, + ``, + `During this housekeeping milestone, we also addressed several feature requests and community [pull requests](#thank-you). Read on to learn about new features and settings.`, + ``, + `## Workbench`, + ``, + `### More prominent pinned tabs`, + ``, + `${CURSOR}Pinned tabs will now always show their pin icon, even while inactive, to make them easier to identify. If an editor is both pinned and contains unsaved changes, the icon reflects both states.`, + ``, + `![Inactive pinned tabs showing pin icons](images/1_51/pinned-tabs.png)` + ) + ); + assertNestedRangesEqual(ranges![0], [27, 0, 27, 201], [26, 0, 29, 70], [25, 0, 29, 70], [24, 0, 29, 70], [23, 0, 29, 70], [10, 0, 29, 70], [9, 0, 29, 70]); + }); }); function assertNestedLineNumbersEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number][]) { const lineage = getLineage(range); - assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length}`); + assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length} ${getValues(lineage)}`); for (let i = 0; i < lineage.length; i++) { assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`); } diff --git a/extensions/npm/src/commands.ts b/extensions/npm/src/commands.ts index 7ca92879f9b..4f9131a44b4 100644 --- a/extensions/npm/src/commands.ts +++ b/extensions/npm/src/commands.ts @@ -15,7 +15,7 @@ import { const localize = nls.loadMessageBundle(); -export function runSelectedScript() { +export function runSelectedScript(context: vscode.ExtensionContext) { let editor = vscode.window.activeTextEditor; if (!editor) { return; @@ -27,15 +27,15 @@ export function runSelectedScript() { let script = findScriptAtPosition(contents, offset); if (script) { - runScript(script, document); + runScript(context, script, document); } else { let message = localize('noScriptFound', 'Could not find a valid npm script at the selection.'); vscode.window.showErrorMessage(message); } } -export async function selectAndRunScriptFromFolder(selectedFolder: vscode.Uri) { - let taskList: FolderTaskItem[] = await detectNpmScriptsForFolder(selectedFolder); +export async function selectAndRunScriptFromFolder(context: vscode.ExtensionContext, selectedFolder: vscode.Uri) { + let taskList: FolderTaskItem[] = await detectNpmScriptsForFolder(context, selectedFolder); if (taskList && taskList.length > 0) { const quickPick = vscode.window.createQuickPick(); diff --git a/extensions/npm/src/npmMain.ts b/extensions/npm/src/npmMain.ts index 91936da6257..568c5ea3d6f 100644 --- a/extensions/npm/src/npmMain.ts +++ b/extensions/npm/src/npmMain.ts @@ -58,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext): Promise })); context.subscriptions.push(vscode.commands.registerCommand('npm.packageManager', (args) => { if (args instanceof vscode.Uri) { - return getPackageManager(args); + return getPackageManager(context, args); } return ''; })); @@ -83,7 +83,7 @@ function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposab let workspaceWatcher = vscode.workspace.onDidChangeWorkspaceFolders((_e) => invalidateScriptCaches()); context.subscriptions.push(workspaceWatcher); - taskProvider = new NpmTaskProvider(); + taskProvider = new NpmTaskProvider(context); let disposable = vscode.tasks.registerTaskProvider('npm', taskProvider); context.subscriptions.push(disposable); return disposable; diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index e7eec0e84fa..c4f3bc3495b 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -14,7 +14,7 @@ import { } from 'vscode'; import * as nls from 'vscode-nls'; import { - createTask, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition, + createTask, getPackageManager, getTaskName, isAutoDetectionEnabled, isWorkspaceFolder, NpmTaskDefinition, NpmTaskProvider, startDebugging, TaskLocation, @@ -132,7 +132,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { private _onDidChangeTreeData: EventEmitter = new EventEmitter(); readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; - constructor(context: ExtensionContext, public taskProvider: NpmTaskProvider) { + constructor(private context: ExtensionContext, public taskProvider: NpmTaskProvider) { const subscriptions = context.subscriptions; this.extensionContext = context; subscriptions.push(commands.registerCommand('npm.runScript', this.runScript, this)); @@ -142,11 +142,13 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { } private async runScript(script: NpmScript) { + // Call getPackageManager to trigger the multiple lock files warning. + await getPackageManager(this.context, script.getFolder().uri); tasks.executeTask(script.task); } private async debugScript(script: NpmScript) { - startDebugging(script.task.definition.script, path.dirname(script.package.resourceUri!.fsPath), script.getFolder()); + startDebugging(this.extensionContext, script.task.definition.script, path.dirname(script.package.resourceUri!.fsPath), script.getFolder()); } private findScript(document: TextDocument, script?: NpmScript): number { @@ -190,7 +192,7 @@ export class NpmScriptsTreeDataProvider implements TreeDataProvider { if (!uri) { return; } - let task = await createTask('install', 'install', selection.folder.workspaceFolder, uri, undefined, []); + let task = await createTask(this.extensionContext, 'install', 'install', selection.folder.workspaceFolder, uri, true, undefined, []); tasks.executeTask(task); } diff --git a/extensions/npm/src/scriptHover.ts b/extensions/npm/src/scriptHover.ts index 01c0c4b8c63..48482660f52 100644 --- a/extensions/npm/src/scriptHover.ts +++ b/extensions/npm/src/scriptHover.ts @@ -30,7 +30,7 @@ export function invalidateHoverScriptsCache(document?: TextDocument) { export class NpmScriptHoverProvider implements HoverProvider { - constructor(context: ExtensionContext) { + constructor(private context: ExtensionContext) { context.subscriptions.push(commands.registerCommand('npm.runScriptFromHover', this.runScriptFromHover, this)); context.subscriptions.push(commands.registerCommand('npm.debugScriptFromHover', this.debugScriptFromHover, this)); context.subscriptions.push(workspace.onDidChangeTextDocument((e) => { @@ -103,7 +103,7 @@ export class NpmScriptHoverProvider implements HoverProvider { let documentUri = args.documentUri; let folder = workspace.getWorkspaceFolder(documentUri); if (folder) { - let task = await createTask(script, `run ${script}`, folder, documentUri); + let task = await createTask(this.context, script, `run ${script}`, folder, documentUri); await tasks.executeTask(task); } } @@ -113,7 +113,7 @@ export class NpmScriptHoverProvider implements HoverProvider { let documentUri = args.documentUri; let folder = workspace.getWorkspaceFolder(documentUri); if (folder) { - startDebugging(script, dirname(documentUri.fsPath), folder); + startDebugging(this.context, script, dirname(documentUri.fsPath), folder); } } } diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 41e8cf45492..509cef08317 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -5,7 +5,7 @@ import { TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, - DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position + DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem, window, Position, ExtensionContext, env } from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; @@ -44,15 +44,15 @@ export interface TaskWithLocation { export class NpmTaskProvider implements TaskProvider { - constructor() { + constructor(private context: ExtensionContext) { } get tasksWithLocation(): Promise { - return provideNpmScripts(); + return provideNpmScripts(this.context, false); } public async provideTasks() { - const tasks = await provideNpmScripts(); + const tasks = await provideNpmScripts(this.context, true); return tasks.map(task => task.task); } @@ -70,7 +70,7 @@ export class NpmTaskProvider implements TaskProvider { } else { packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' }); } - return createTask(kind, `${kind.script === INSTALL_SCRIPT ? '' : 'run '}${kind.script}`, _task.scope, packageJsonUri); + return createTask(this.context, kind, `${kind.script === INSTALL_SCRIPT ? '' : 'run '}${kind.script}`, _task.scope, packageJsonUri); } return undefined; } @@ -123,16 +123,23 @@ export function isWorkspaceFolder(value: any): value is WorkspaceFolder { return value && typeof value !== 'number'; } -export async function getPackageManager(folder: Uri): Promise { +export async function getPackageManager(extensionContext: ExtensionContext, folder: Uri, showWarning: boolean = true): Promise { let packageManagerName = workspace.getConfiguration('npm', folder).get('packageManager', 'npm'); if (packageManagerName === 'auto') { const { name, multiplePMDetected } = await findPreferredPM(folder.fsPath); packageManagerName = name; - - if (multiplePMDetected) { - const multiplePMWarning = localize('npm.multiplePMWarning', 'Found multiple lockfiles for {0}. Using {1} as the preferred package manager.', folder.fsPath, packageManagerName); - window.showWarningMessage(multiplePMWarning); + const neverShowWarning = 'npm.multiplePMWarning.neverShow'; + if (showWarning && multiplePMDetected && !extensionContext.globalState.get(neverShowWarning)) { + const multiplePMWarning = localize('npm.multiplePMWarning', 'Using {0} as the preferred package manager. Found multiple lockfiles for {1}.', packageManagerName, folder.fsPath); + const neverShowAgain = localize('npm.multiplePMWarning.doNotShow', "Do not show again"); + const learnMore = localize('npm.multiplePMWarning.learnMore', "Learn more"); + window.showInformationMessage(multiplePMWarning, learnMore, neverShowAgain).then(result => { + switch (result) { + case neverShowAgain: extensionContext.globalState.update(neverShowWarning, true); break; + case learnMore: env.openExternal(Uri.parse('https://nodejs.dev/learn/the-package-lock-json-file')); + } + }); } } @@ -160,7 +167,7 @@ export async function hasNpmScripts(): Promise { } } -async function detectNpmScripts(): Promise { +async function detectNpmScripts(context: ExtensionContext, showWarning: boolean): Promise { let emptyTasks: TaskWithLocation[] = []; let allTasks: TaskWithLocation[] = []; @@ -177,7 +184,7 @@ async function detectNpmScripts(): Promise { let paths = await workspace.findFiles(relativePattern, '**/{node_modules,.vscode-test}/**'); for (const path of paths) { if (!isExcluded(folder, path) && !visitedPackageJsonFiles.has(path.fsPath)) { - let tasks = await provideNpmScriptsForFolder(path); + let tasks = await provideNpmScriptsForFolder(context, path, showWarning); visitedPackageJsonFiles.add(path.fsPath); allTasks.push(...tasks); } @@ -191,7 +198,7 @@ async function detectNpmScripts(): Promise { } -export async function detectNpmScriptsForFolder(folder: Uri): Promise { +export async function detectNpmScriptsForFolder(context: ExtensionContext, folder: Uri): Promise { let folderTasks: FolderTaskItem[] = []; @@ -202,7 +209,7 @@ export async function detectNpmScriptsForFolder(folder: Uri): Promise = new Set(); for (const path of paths) { if (!visitedPackageJsonFiles.has(path.fsPath)) { - let tasks = await provideNpmScriptsForFolder(path); + let tasks = await provideNpmScriptsForFolder(context, path, true); visitedPackageJsonFiles.add(path.fsPath); folderTasks.push(...tasks.map(t => ({ label: t.task.name, task: t.task }))); } @@ -213,9 +220,9 @@ export async function detectNpmScriptsForFolder(folder: Uri): Promise { +export async function provideNpmScripts(context: ExtensionContext, showWarning: boolean): Promise { if (!cachedTasks) { - cachedTasks = await detectNpmScripts(); + cachedTasks = await detectNpmScripts(context, showWarning); } return cachedTasks; } @@ -251,7 +258,7 @@ function isDebugScript(script: string): boolean { return match !== null; } -async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise { +async function provideNpmScriptsForFolder(context: ExtensionContext, packageJsonUri: Uri, showWarning: boolean): Promise { let emptyTasks: TaskWithLocation[] = []; let folder = workspace.getWorkspaceFolder(packageJsonUri); @@ -269,7 +276,7 @@ async function provideNpmScriptsForFolder(packageJsonUri: Uri): Promise { +export async function createTask(context: ExtensionContext, script: NpmTaskDefinition | string, cmd: string, folder: WorkspaceFolder, packageJsonUri: Uri, showWarning: boolean = true, detail?: string, matcher?: any): Promise { let kind: NpmTaskDefinition; if (typeof script === 'string') { kind = { type: 'npm', script: script }; @@ -307,7 +314,7 @@ export async function createTask(script: NpmTaskDefinition | string, cmd: string kind = script; } - const packageManager = await getPackageManager(folder.uri); + const packageManager = await getPackageManager(context, folder.uri, showWarning); async function getCommandLine(cmd: string): Promise { if (workspace.getConfiguration('npm', folder.uri).get('runSilent')) { return `${packageManager} --silent ${cmd}`; @@ -368,22 +375,22 @@ async function exists(file: string): Promise { }); } -export async function runScript(script: string, document: TextDocument) { +export async function runScript(context: ExtensionContext, script: string, document: TextDocument) { let uri = document.uri; let folder = workspace.getWorkspaceFolder(uri); if (folder) { - let task = await createTask(script, `run ${script}`, folder, uri); + let task = await createTask(context, script, `run ${script}`, folder, uri); tasks.executeTask(task); } } -export async function startDebugging(scriptName: string, cwd: string, folder: WorkspaceFolder) { +export async function startDebugging(context: ExtensionContext, scriptName: string, cwd: string, folder: WorkspaceFolder) { const config: DebugConfiguration = { type: 'pwa-node', request: 'launch', name: `Debug ${scriptName}`, cwd, - runtimeExecutable: await getPackageManager(folder.uri), + runtimeExecutable: await getPackageManager(context, folder.uri), runtimeArgs: [ 'run', scriptName, diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index d0382cec294..ea03fb3fa41 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -9,21 +9,14 @@ "statusBarItem.remoteBackground": "#00000000", "sideBarTitle.foreground": "#FFFFFF" }, - "settings": [ - { - "settings": { - "foreground": "#FFFFFF", - "background": "#000000" - } - }, + "tokenColors": [ { "scope": [ "meta.embedded", "source.groovy.embedded" ], "settings": { - "foreground": "#FFFFFF", - "background": "#000000" + "foreground": "#FFFFFF" } }, { diff --git a/package.json b/package.json index c4a391eee6f..13c521e2dec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.52.0", - "distro": "7c9caf8254b4e707f8f7637661df05d1397f2181", + "version": "1.53.0", + "distro": "1aaf2adfd4fd7c35c133724c4d97e103f2fa331f", "author": { "name": "Microsoft Corporation" }, @@ -45,7 +45,7 @@ "compile-web": "gulp compile-web --max_old_space_size=4095", "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions", - "electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.0.2" + "electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.0.3" }, "dependencies": { "applicationinsights": "1.0.8", @@ -60,7 +60,7 @@ "native-is-elevated": "0.4.1", "native-keymap": "2.2.1", "native-watchdog": "1.3.0", - "node-pty": "0.10.0-beta17", + "node-pty": "0.10.0-beta18", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", "tas-client-umd": "0.1.2", @@ -112,7 +112,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "9.3.5", + "electron": "11.0.3", "electron-rebuild": "2.0.3", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", @@ -176,7 +176,7 @@ "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vsce": "1.48.0", - "vscode-debugprotocol": "1.41.0", + "vscode-debugprotocol": "1.43.0", "vscode-nls-dev": "^3.3.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", @@ -191,7 +191,7 @@ "url": "https://github.com/microsoft/vscode/issues" }, "optionalDependencies": { - "vscode-windows-ca-certs": "0.2.0", + "vscode-windows-ca-certs": "^0.3.0", "vscode-windows-registry": "1.0.3", "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", diff --git a/product.json b/product.json index f62018b387c..207bcf8f512 100644 --- a/product.json +++ b/product.json @@ -31,7 +31,7 @@ "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.14", + "version": "1.44.15", "repo": "https://github.com/microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -91,7 +91,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.51.0", + "version": "1.52.2", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.yarnrc b/remote/.yarnrc index c1a32ce532a..cd436416b56 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,3 +1,3 @@ disturl "http://nodejs.org/dist" -target "12.14.1" +target "12.18.3" runtime "node" diff --git a/remote/package.json b/remote/package.json index f68deac647c..57710a189ba 100644 --- a/remote/package.json +++ b/remote/package.json @@ -12,15 +12,15 @@ "jschardet": "2.2.1", "minimist": "^1.2.5", "native-watchdog": "1.3.0", - "node-pty": "0.10.0-beta17", + "node-pty": "0.10.0-beta18", "spdlog": "^0.11.1", "tas-client-umd": "0.1.2", "vscode-nsfw": "1.2.9", "vscode-oniguruma": "1.3.1", "vscode-proxy-agent": "^0.5.2", + "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.1", "vscode-textmate": "5.2.0", - "vscode-regexpp": "^3.1.0", "xterm": "4.10.0-beta.4", "xterm-addon-search": "0.8.0-beta.3", "xterm-addon-unicode11": "0.3.0-beta.3", @@ -29,7 +29,7 @@ "yazl": "^2.4.3" }, "optionalDependencies": { - "vscode-windows-ca-certs": "0.2.0", + "vscode-windows-ca-certs": "0.3.0", "vscode-windows-registry": "1.0.2" } } diff --git a/remote/yarn.lock b/remote/yarn.lock index 22d8ebe3fc5..e15eb9d9839 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -299,15 +299,15 @@ native-watchdog@1.3.0: resolved "https://registry.yarnpkg.com/native-watchdog/-/native-watchdog-1.3.0.tgz#88cee94c9dc766b85c8506eda14c8bd8c9618e27" integrity sha512-WOjGRNGkYZ5MXsntcvCYrKtSYMaewlbCFplbcUVo9bE80LPVt8TAVFHYWB8+a6fWCGYheq21+Wtt6CJrUaCJhw== -node-addon-api@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.2.tgz#d8aad9781a5cfc4132cc2fecdbdd982534265217" - integrity sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA== +node-addon-api@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681" + integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg== -node-pty@0.10.0-beta17: - version "0.10.0-beta17" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0-beta17.tgz#962d4a3f4dc6772385e0cad529c209cef3bc79e6" - integrity sha512-tn7EANQacnAvnOQCImvgag1DL0tVmUoY/1yIZbh3u/BBpvCcGHLZJNn7TXheodRLr6hmGSUS2VbfcUr9p0gOug== +node-pty@0.10.0-beta18: + version "0.10.0-beta18" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.10.0-beta18.tgz#7ed2d3f4a06b2b23fe2abdf5b41655e9dffc25d5" + integrity sha512-vpK4yB3A3VzgkvdOWegL7GcPapt45jfA4b3ejUe8k4RmqdWBRvFJngew8T3qAxmLhTkfo93psaN6izTlfkc6FA== dependencies: nan "^2.14.0" @@ -438,12 +438,12 @@ vscode-textmate@5.2.0: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.2.0.tgz#01f01760a391e8222fe4f33fbccbd1ad71aed74e" integrity sha512-Uw5ooOQxRASHgu6C7GVvUxisKXfSgW4oFlO+aa+PAkgmH89O3CXxEEzNRNtHSqtXFTl0nAC1uYj0GMSH27uwtQ== -vscode-windows-ca-certs@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.2.0.tgz#086f0f4de57e2760a35ac6920831bff246237115" - integrity sha512-YBrJRT0zos+Yb1Qdn73GD8QZr7pa2IE96b5Y1hmmp6XeR8aYB7Iiq5gDAF/+/AxL+caSR9KPZQ6jiYWh5biD7w== +vscode-windows-ca-certs@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/vscode-windows-ca-certs/-/vscode-windows-ca-certs-0.3.0.tgz#324e1f8ba842bbf048a39e7c0ee8fe655e9adfcc" + integrity sha512-CYrpCEKmAFQJoZNReOrelNL+VKyebOVRCqL9evrBlVcpWQDliliJgU5RggGS8FPGtQ3jAKLQt9frF0qlxYYPKA== dependencies: - node-addon-api "1.6.2" + node-addon-api "^3.0.2" vscode-windows-registry@1.0.2: version "1.0.2" diff --git a/resources/linux/debian/postinst.template b/resources/linux/debian/postinst.template index 9f26b350999..e1b201b4f9a 100755 --- a/resources/linux/debian/postinst.template +++ b/resources/linux/debian/postinst.template @@ -73,6 +73,6 @@ NdCFTW7wY0Fb1fWJ+/KTsC4= if [ "$WRITE_SOURCE" -eq "1" ]; then echo "### THIS FILE IS AUTOMATICALLY CONFIGURED ### # You may comment out this entry, but any other modifications may be lost. -deb [arch=amd64] http://packages.microsoft.com/repos/vscode stable main" > $CODE_SOURCE_PART +deb [arch=amd64] http://packages.microsoft.com/repos/@@REPOSITORY_NAME@@ stable main" > $CODE_SOURCE_PART fi fi diff --git a/resources/web/code-web.js b/resources/web/code-web.js index 9be3bc2d811..11a9f87e730 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -377,11 +377,18 @@ async function handleRoot(req, res) { fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`); } + const secondaryHost = ( + req.headers['host'] + ? req.headers['host'].replace(':' + PORT, ':' + SECONDARY_PORT) + : `${HOST}:${SECONDARY_PORT}` + ); const webConfigJSON = { folderUri: folderUri, staticExtensions, - enableSyncByDefault: args['enable-sync'], - webWorkerExtensionHostIframeSrc: `${SCHEME}://${HOST}:${SECONDARY_PORT}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` + settingsSyncOptions: { + enabled: args['enable-sync'] + }, + webWorkerExtensionHostIframeSrc: `${SCHEME}://${secondaryHost}/static/out/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html` }; if (args['wrap-iframe']) { webConfigJSON._wrapWebWorkerExtHostInIframe = true; diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index b302ce28db9..609a2bf6121 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -46,21 +46,48 @@ else echo "Running integration tests with '$INTEGRATION_TEST_ELECTRON_PATH' as build." fi +if [ -z "$INTEGRATION_TEST_APP_NAME" ]; then + after_suite() { true; } +else + after_suite() { killall $INTEGRATION_TEST_APP_NAME || true; } +fi + # Integration tests in AMD ./scripts/test.sh --runGlob **/*.integrationTest.js "$@" +after_suite # Tests in the extension host "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite + +# TODO(deepak1556): Disable workspace test temporarily +# https://github.com/microsoft/vscode/issues/111288 +#"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +#after_suite + "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite + "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite + #"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +# after_suite + "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/out/test/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite + "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite + "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR +after_suite # Tests in commonJS (CSS, HTML) cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js +after_suite + cd $ROOT/extensions/html-language-features/server && $ROOT/scripts/node-electron.sh test/index.js +after_suite rm -rf $VSCODEUSERDATADIR diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index 68e2219bad8..2787af39e18 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -135,18 +135,47 @@ function pipeLoggingToParent() { && !(obj instanceof Date); } + /** + * + * @param {'log' | 'warn' | 'error'} severity + * @param {string} args + */ + function safeSendConsoleMessage(severity, args) { + safeSend({ type: '__$console', severity, arguments: args }); + } + + /** + * @param {'log' | 'info' | 'warn' | 'error'} method + * @param {'log' | 'warn' | 'error'} severity + */ + function wrapConsoleMethod(method, severity) { + if (process.env.VSCODE_LOG_NATIVE === 'true') { + const original = console[method]; + console[method] = function () { + safeSendConsoleMessage(severity, safeToArray(arguments)); + + const stream = method === 'error' || method === 'warn' ? process.stderr : process.stdout; + stream.write('\nSTART_NATIVE_LOG\n'); + original.apply(console, arguments); + stream.write('\nEND_NATIVE_LOG\n'); + }; + } else { + console[method] = function () { safeSendConsoleMessage(severity, safeToArray(arguments)); }; + } + } + // Pass console logging to the outside so that we have it in the main side if told so if (process.env.VERBOSE_LOGGING === 'true') { - console.log = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); }; - console.info = function () { safeSend({ type: '__$console', severity: 'log', arguments: safeToArray(arguments) }); }; - console.warn = function () { safeSend({ type: '__$console', severity: 'warn', arguments: safeToArray(arguments) }); }; - } else { + wrapConsoleMethod('info', 'log'); + wrapConsoleMethod('log', 'log'); + wrapConsoleMethod('warn', 'warn'); + wrapConsoleMethod('error', 'error'); + } else if (process.env.VSCODE_LOG_NATIVE !== 'true') { console.log = function () { /* ignore */ }; console.warn = function () { /* ignore */ }; console.info = function () { /* ignore */ }; + wrapConsoleMethod('error', 'error'); } - - console.error = function () { safeSend({ type: '__$console', severity: 'error', arguments: safeToArray(arguments) }); }; } function handleExceptions() { diff --git a/src/buildfile.js b/src/buildfile.js index f6d1c647d9d..c0400de76c0 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -10,7 +10,7 @@ function entrypoint(name) { exports.base = [{ name: 'vs/base/common/worker/simpleWorker', include: ['vs/editor/common/services/editorSimpleWorker'], - prepend: ['vs/loader.js'], + prepend: ['vs/loader.js', 'vs/nls.js'], append: ['vs/base/worker/workerMain'], dest: 'vs/base/worker/workerMain.js' }]; diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index b4740dadea0..b1835429f3c 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -451,6 +451,8 @@ export interface IDimension { export class Dimension implements IDimension { + static readonly None = new Dimension(0, 0); + constructor( public readonly width: number, public readonly height: number, @@ -1398,7 +1400,12 @@ function toBinary(str: string): string { for (let i = 0; i < codeUnits.length; i++) { codeUnits[i] = str.charCodeAt(i); } - return String.fromCharCode(...new Uint8Array(codeUnits.buffer)); + let binary = ''; + const uint8array = new Uint8Array(codeUnits.buffer); + for (let i = 0; i < uint8array.length; i++) { + binary += String.fromCharCode(uint8array[i]); + } + return binary; } /** diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index b996d599700..50274475e84 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -14,7 +14,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { isString } from 'vs/base/common/types'; +import { isFunction, isString } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; export interface IIconLabelCreationOptions { @@ -25,7 +25,7 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelMarkdownString { - markdown: IMarkdownString | string | undefined | Promise; + markdown: IMarkdownString | string | undefined | (() => Promise); markdownNotSupportedFallback: string | undefined; } @@ -191,23 +191,39 @@ export class IconLabel extends Disposable { } private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void { + htmlElement.setAttribute('title', ''); htmlElement.removeAttribute('title'); - let tooltip = isString(markdownTooltip) ? markdownTooltip : markdownTooltip.markdown; + let tooltip: () => Promise; + if (isString(markdownTooltip)) { + tooltip = async () => markdownTooltip; + } else if (isFunction(markdownTooltip.markdown)) { + tooltip = markdownTooltip.markdown; + } else { + const markdown = markdownTooltip.markdown; + tooltip = async () => markdown; + } // Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely. // On Mac, the delay is 1500. const hoverDelay = isMacintosh ? 1500 : 500; let hoverOptions: IHoverDelegateOptions | undefined; let mouseX: number | undefined; + let isHovering = false; function mouseOver(this: HTMLElement, e: MouseEvent): any { - let isHovering = true; - function mouseMove(this: HTMLElement, e: MouseEvent): any { - mouseX = e.x; + if (isHovering) { + return; } function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any { isHovering = false; + mouseLeaveDisposable.dispose(); + mouseDownDisposable.dispose(); } const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement)); const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement)); + isHovering = true; + + function mouseMove(this: HTMLElement, e: MouseEvent): any { + mouseX = e.x; + } const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement)); setTimeout(async () => { if (isHovering && tooltip) { @@ -217,7 +233,7 @@ export class IconLabel extends Disposable { targetElements: [this], dispose: () => { } }; - const resolvedTooltip = await tooltip; + const resolvedTooltip = await tooltip(); if (resolvedTooltip) { hoverOptions = { text: resolvedTooltip, @@ -226,7 +242,8 @@ export class IconLabel extends Disposable { }; } } - if (hoverOptions) { + // awaiting the tooltip could take a while. Make sure we're still hovering. + if (hoverOptions && isHovering) { if (mouseX !== undefined) { (hoverOptions.target).x = mouseX + 10; } @@ -234,8 +251,6 @@ export class IconLabel extends Disposable { } } mouseMoveDisposable.dispose(); - mouseLeaveDisposable.dispose(); - mouseDownDisposable.dispose(); }, hoverDelay); } const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement))); diff --git a/src/vs/base/browser/ui/list/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index 03db9d2a60d..e2e8e849aa2 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -258,6 +258,10 @@ export class PagedList implements IThemable, IDisposable { return this.list.getSelection(); } + getSelectedElements(): T[] { + return this.getSelection().map(i => this.model.get(i)); + } + layout(height?: number, width?: number): void { this.list.layout(height, width); } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index f0e111e3f15..4df19745cfe 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1117,9 +1117,7 @@ class TreeNodeListMouseController extends MouseController< expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick; } - const clickedOnFocus = this.tree.getFocus()[0] === node.element; - - if (expandOnlyOnTwistieClick && !onTwistie && e.browserEvent.detail !== 2 && !(clickedOnFocus && !node.collapsed)) { + if (expandOnlyOnTwistieClick && !onTwistie && e.browserEvent.detail !== 2) { return super.onViewPointer(e); } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 37e3c3bc570..0e08876e537 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -377,7 +377,8 @@ export class TernarySearchTree { } has(key: K): boolean { - return !!this._getNode(key); + const node = this._getNode(key); + return !(node?.value === undefined && node?.mid === undefined); } delete(key: K): void { diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 3a17bd3fc78..fa07dcb9b7e 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -459,13 +459,13 @@ export function whenDeleted(path: string): Promise { export async function move(source: string, target: string): Promise { if (source === target) { - return Promise.resolve(); + return; } async function updateMtime(path: string): Promise { const stat = await lstat(path); if (stat.isDirectory() || stat.isSymbolicLink()) { - return Promise.resolve(); // only for files + return; // only for files } const fd = await promisify(fs.open)(path, 'a'); @@ -510,7 +510,7 @@ export async function copy(source: string, target: string, copiedSourcesIn?: { [ } if (copiedSources[source]) { - return Promise.resolve(); // escape when there are cycles (can happen with symlinks) + return; // escape when there are cycles (can happen with symlinks) } copiedSources[source] = true; // remember as copied diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index ba8bbd0239f..0973f0c1446 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -190,12 +190,11 @@ class ListElementRenderer implements IListRenderer { + const action = new Action(`id-${index}`, '', cssClasses, true, async () => { element.fireButtonTriggered({ button, item: element.item }); - return Promise.resolve(); }); action.tooltip = button.tooltip || ''; return action; diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index 8b55819cc54..c2f5ff5a5cd 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -200,9 +200,9 @@ export class Storage extends Disposable implements IStorage { return parseInt(value, 10); } - set(key: string, value: string | boolean | number | null | undefined): Promise { + async set(key: string, value: string | boolean | number | null | undefined): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // Return early if we are already closed + return; // Return early if we are already closed } // We remove the key for undefined/null values @@ -216,7 +216,7 @@ export class Storage extends Disposable implements IStorage { // Return early if value already set const currentValue = this.cache.get(key); if (currentValue === valueStr) { - return Promise.resolve(); + return; } // Update in cache and pending @@ -231,15 +231,15 @@ export class Storage extends Disposable implements IStorage { return this.flushDelayer.trigger(() => this.flushPending()); } - delete(key: string): Promise { + async delete(key: string): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // Return early if we are already closed + return; // Return early if we are already closed } // Remove from cache and add to pending const wasDeleted = this.cache.delete(key); if (!wasDeleted) { - return Promise.resolve(); // Return early if value already deleted + return; // Return early if value already deleted } if (!this.pendingDeletes.has(key)) { @@ -257,7 +257,7 @@ export class Storage extends Disposable implements IStorage { async close(): Promise { if (this.state === StorageState.Closed) { - return Promise.resolve(); // return if already closed + return; // return if already closed } // Update state @@ -282,9 +282,9 @@ export class Storage extends Disposable implements IStorage { return this.pendingInserts.size > 0 || this.pendingDeletes.size > 0; } - private flushPending(): Promise { + private async flushPending(): Promise { if (!this.hasPending) { - return Promise.resolve(); // return early if nothing to do + return; // return early if nothing to do } // Get pending data @@ -305,9 +305,9 @@ export class Storage extends Disposable implements IStorage { }); } - whenFlushed(): Promise { + async whenFlushed(): Promise { if (!this.hasPending) { - return Promise.resolve(); // return early if nothing to do + return; // return early if nothing to do } return new Promise(resolve => this.whenFlushedCallbacks.push(resolve)); diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 99b4f1b90fc..12a7c3ca7c8 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -73,8 +73,9 @@ suite('dom', () => { }); test('multibyteAwareBtoa', () => { - assert.equal(dom.multibyteAwareBtoa('hello world'), dom.multibyteAwareBtoa('hello world')); - assert.ok(dom.multibyteAwareBtoa('平仮名')); + assert.ok(dom.multibyteAwareBtoa('hello world').length > 0); + assert.ok(dom.multibyteAwareBtoa('平仮名').length > 0); + assert.ok(dom.multibyteAwareBtoa(new Array(100000).fill('vs').join('')).length > 0); // https://github.com/microsoft/vscode/issues/112013 }); suite('$', () => { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 759c85bc747..6eb685b7926 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -7,7 +7,7 @@ import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindo import { IProcessEnvironment, isWindows, isMacintosh, isLinux } from 'vs/base/common/platform'; import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { resolveShellEnv } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; @@ -34,7 +34,6 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; -import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2'; import { FileProtocolHandler } from 'vs/code/electron-main/protocol'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; @@ -71,7 +70,7 @@ import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/ext import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { INativeHostMainService, NativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; -import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; @@ -454,12 +453,8 @@ export class CodeApplication extends Disposable { this._register(server); } - // Setup Auth Handler (TODO@ben remove old auth handler eventually) - if (this.configurationService.getValue('window.enableExperimentalProxyLoginDialog') === false) { - this._register(new ProxyAuthHandler()); - } else { - this._register(appInstantiationService.createInstance(ProxyAuthHandler2)); - } + // Setup Auth Handler + this._register(appInstantiationService.createInstance(ProxyAuthHandler)); // Open Windows const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler)); @@ -902,7 +897,12 @@ export class CodeApplication extends Disposable { this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen; // Remote Authorities - this.handleRemoteAuthorities(); + protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { + callback({ + url: request.url.replace(/^vscode-remote-resource:/, 'http:'), + method: request.method + }); + }); // Initialize update service const updateService = accessor.get(IUpdateService); @@ -934,19 +934,11 @@ export class CodeApplication extends Disposable { '}' ]; const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n')); + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString)); } } catch (error) { this.logService.error(error); } } - - private handleRemoteAuthorities(): void { - protocol.registerHttpProtocol(Schemas.vscodeRemoteResource, (request, callback) => { - callback({ - url: request.url.replace(/^vscode-remote-resource:/, 'http:'), - method: request.method - }); - }); - } } diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index b4096018623..b9669f983c7 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -3,18 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { FileAccess } from 'vs/base/common/network'; -import { BrowserWindow, BrowserWindowConstructorOptions, app, AuthInfo, WebContents, Event as ElectronEvent } from 'electron'; +import { hash } from 'vs/base/common/hash'; +import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; +import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; +import { generateUuid } from 'vs/base/common/uuid'; +import product from 'vs/platform/product/common/product'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { + firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 +} type LoginEvent = { event: ElectronEvent; - webContents: WebContents; - req: Request; authInfo: AuthInfo; - cb: (username: string, password: string) => void; + req: ElectronAuthenticationResponseDetails; + + callback: (username?: string, password?: string) => void; }; type Credentials = { @@ -22,81 +32,211 @@ type Credentials = { password: string; }; +enum ProxyAuthState { + + /** + * Initial state: we will try to use stored credentials + * first to reply to the auth challenge. + */ + Initial = 1, + + /** + * We used stored credentials and are still challenged, + * so we will show a login dialog next. + */ + StoredCredentialsUsed, + + /** + * Finally, if we showed a login dialog already, we will + * not show any more login dialogs until restart to reduce + * the UI noise. + */ + LoginDialogShown +} + export class ProxyAuthHandler extends Disposable { - declare readonly _serviceBrand: undefined; + private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - private retryCount = 0; + private pendingProxyResolve: Promise | undefined = undefined; - constructor() { + private state = ProxyAuthState.Initial; + + private sessionCredentials: Credentials | undefined = undefined; + + constructor( + @ILogService private readonly logService: ILogService, + @IWindowsMainService private readonly windowsMainService: IWindowsMainService, + @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, + @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService + ) { super(); this.registerListeners(); } private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event, webContents, req, authInfo, cb) => ({ event, webContents, req, authInfo, cb })); + const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); this._register(onLogin(this.onLogin, this)); } - private onLogin({ event, authInfo, cb }: LoginEvent): void { + private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { if (!authInfo.isProxy) { - return; + return; // only for proxy } - if (this.retryCount++ > 1) { - return; + if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { + this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); + + return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) } + // Signal we handle this event on our own, otherwise + // Electron will ignore our provided credentials. event.preventDefault(); - const opts: BrowserWindowConstructorOptions = { - alwaysOnTop: true, - skipTaskbar: true, - resizable: false, - width: 450, - height: 225, - show: true, - title: 'VS Code', - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - sandbox: true, - contextIsolation: true, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - devTools: false - } - }; + let credentials: Credentials | undefined = undefined; + if (!this.pendingProxyResolve) { + this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - opts.parent = focusedWindow; - opts.modal = true; + this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); + try { + credentials = await this.pendingProxyResolve; + } finally { + this.pendingProxyResolve = undefined; + } + } else { + this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); + + credentials = await this.pendingProxyResolve; } - const win = new BrowserWindow(opts); - const windowUrl = FileAccess.asBrowserUri('vs/code/electron-sandbox/proxy/auth.html', require); - const proxyUrl = `${authInfo.host}:${authInfo.port}`; - const title = localize('authRequire', "Proxy Authentication Required"); - const message = localize('proxyauth', "The proxy {0} requires authentication.", proxyUrl); + // According to Electron docs, it is fine to call back without + // username or password to signal that the authentication was handled + // by us, even though without having credentials received: + // + // > If `callback` is called without a username or password, the authentication + // > request will be cancelled and the authentication error will be returned to the + // > page. + callback(credentials?.username, credentials?.password); + } - const onWindowClose = () => cb('', ''); - win.on('close', onWindowClose); + private async resolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - win.setMenu(null); - win.webContents.on('did-finish-load', () => { - const data = { title, message }; - win.webContents.send('vscode:openProxyAuthDialog', data); - }); - win.webContents.on('ipc-message', (event, channel, credentials: Credentials) => { - if (channel === 'vscode:proxyAuthResponse') { - const { username, password } = credentials; - cb(username, password); - win.removeListener('close', onWindowClose); - win.close(); + try { + const credentials = await this.doResolveProxyCredentials(authInfo); + if (credentials) { + this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); + + return credentials; + } else { + this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); } + } finally { + this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); + } + + return undefined; + } + + private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { + this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); + + // Compute a hash over the authentication info to be used + // with the credentials store to return the right credentials + // given the properties of the auth request + // (see https://github.com/microsoft/vscode/issues/109497) + const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); + + // Find any previously stored credentials + let storedUsername: string | undefined = undefined; + let storedPassword: string | undefined = undefined; + try { + const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + if (encryptedSerializedProxyCredentials) { + const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); + + storedUsername = credentials.username; + storedPassword = credentials.password; + } + } catch (error) { + this.logService.error(error); // handle errors by asking user for login via dialog + } + + // Reply with stored credentials unless we used them already. + // In that case we need to show a login dialog again because + // they seem invalid. + if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); + this.state = ProxyAuthState.StoredCredentialsUsed; + + return { username: storedUsername, password: storedPassword }; + } + + // Find suitable window to show dialog: prefer to show it in the + // active window because any other network request will wait on + // the credentials and we want the user to present the dialog. + const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); + if (!window) { + this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); + + return undefined; // unexpected + } + + this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); + + // Open proxy dialog + const payload = { + authInfo, + username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored + password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored + replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` + }; + window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); + this.state = ProxyAuthState.LoginDialogShown; + + // Handle reply + const loginDialogCredentials = await new Promise(resolve => { + const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { + if (channel === payload.replyChannel) { + this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); + window.win.webContents.off('ipc-message', proxyAuthResponseHandler); + + // We got credentials from the window + if (reply) { + const credentials: Credentials = { username: reply.username, password: reply.password }; + + // Update stored credentials based on `remember` flag + try { + if (reply.remember) { + const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); + await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + } else { + await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + } + } catch (error) { + this.logService.error(error); // handle gracefully + } + + resolve({ username: credentials.username, password: credentials.password }); + } + + // We did not get any credentials from the window (e.g. cancelled) + else { + resolve(undefined); + } + } + }; + + window.win.webContents.on('ipc-message', proxyAuthResponseHandler); }); - win.loadURL(windowUrl.toString(true)); + + // Remember credentials for the session in case + // the credentials are wrong and we show the dialog + // again + this.sessionCredentials = loginDialogCredentials; + + return loginDialogCredentials; } } diff --git a/src/vs/code/electron-main/auth2.ts b/src/vs/code/electron-main/auth2.ts deleted file mode 100644 index 1b84d4bc4d9..00000000000 --- a/src/vs/code/electron-main/auth2.ts +++ /dev/null @@ -1,242 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { app, AuthInfo, WebContents, Event as ElectronEvent, AuthenticationResponseDetails } from 'electron'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService'; -import { IEncryptionMainService } from 'vs/platform/encryption/electron-main/encryptionMainService'; -import { generateUuid } from 'vs/base/common/uuid'; -import product from 'vs/platform/product/common/product'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { - firstAuthAttempt?: boolean; // https://github.com/electron/electron/blob/84a42a050e7d45225e69df5bd2d2bf9f1037ea41/shell/browser/login_handler.cc#L70 -} - -type LoginEvent = { - event: ElectronEvent; - authInfo: AuthInfo; - req: ElectronAuthenticationResponseDetails; - - callback: (username?: string, password?: string) => void; -}; - -type Credentials = { - username: string; - password: string; -}; - -enum ProxyAuthState { - - /** - * Initial state: we will try to use stored credentials - * first to reply to the auth challenge. - */ - Initial = 1, - - /** - * We used stored credentials and are still challenged, - * so we will show a login dialog next. - */ - StoredCredentialsUsed, - - /** - * Finally, if we showed a login dialog already, we will - * not show any more login dialogs until restart to reduce - * the UI noise. - */ - LoginDialogShown -} - -export class ProxyAuthHandler2 extends Disposable { - - private static PROXY_CREDENTIALS_SERVICE_KEY = `${product.urlProtocol}.proxy-credentials`; - - private pendingProxyResolve: Promise | undefined = undefined; - - private state = ProxyAuthState.Initial; - - private sessionCredentials: Credentials | undefined = undefined; - - constructor( - @ILogService private readonly logService: ILogService, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @INativeHostMainService private readonly nativeHostMainService: INativeHostMainService, - @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - const onLogin = Event.fromNodeEventEmitter(app, 'login', (event: ElectronEvent, webContents: WebContents, req: ElectronAuthenticationResponseDetails, authInfo: AuthInfo, callback) => ({ event, webContents, req, authInfo, callback })); - this._register(onLogin(this.onLogin, this)); - } - - private async onLogin({ event, authInfo, req, callback }: LoginEvent): Promise { - if (!authInfo.isProxy) { - return; // only for proxy - } - - if (!this.pendingProxyResolve && this.state === ProxyAuthState.LoginDialogShown && req.firstAuthAttempt) { - this.logService.trace('auth#onLogin (proxy) - exit - proxy dialog already shown'); - - return; // only one dialog per session at max (except when firstAuthAttempt: false which indicates a login problem) - } - - // Signal we handle this event on our own, otherwise - // Electron will ignore our provided credentials. - event.preventDefault(); - - let credentials: Credentials | undefined = undefined; - if (!this.pendingProxyResolve) { - this.logService.trace('auth#onLogin (proxy) - no pending proxy handling found, starting new'); - - this.pendingProxyResolve = this.resolveProxyCredentials(authInfo); - try { - credentials = await this.pendingProxyResolve; - } finally { - this.pendingProxyResolve = undefined; - } - } else { - this.logService.trace('auth#onLogin (proxy) - pending proxy handling found'); - - credentials = await this.pendingProxyResolve; - } - - // According to Electron docs, it is fine to call back without - // username or password to signal that the authentication was handled - // by us, even though without having credentials received: - // - // > If `callback` is called without a username or password, the authentication - // > request will be cancelled and the authentication error will be returned to the - // > page. - callback(credentials?.username, credentials?.password); - } - - private async resolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#resolveProxyCredentials (proxy) - enter'); - - try { - const credentials = await this.doResolveProxyCredentials(authInfo); - if (credentials) { - this.logService.trace('auth#resolveProxyCredentials (proxy) - got credentials'); - - return credentials; - } else { - this.logService.trace('auth#resolveProxyCredentials (proxy) - did not get credentials'); - } - } finally { - this.logService.trace('auth#resolveProxyCredentials (proxy) - exit'); - } - - return undefined; - } - - private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { - this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); - - // Compute a hash over the authentication info to be used - // with the credentials store to return the right credentials - // given the properties of the auth request - // (see https://github.com/microsoft/vscode/issues/109497) - const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); - - // Find any previously stored credentials - let storedUsername: string | undefined = undefined; - let storedPassword: string | undefined = undefined; - try { - const encryptedSerializedProxyCredentials = await this.nativeHostMainService.getPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - if (encryptedSerializedProxyCredentials) { - const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); - - storedUsername = credentials.username; - storedPassword = credentials.password; - } - } catch (error) { - this.logService.error(error); // handle errors by asking user for login via dialog - } - - // Reply with stored credentials unless we used them already. - // In that case we need to show a login dialog again because - // they seem invalid. - if (this.state !== ProxyAuthState.StoredCredentialsUsed && typeof storedUsername === 'string' && typeof storedPassword === 'string') { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - found stored credentials to use'); - this.state = ProxyAuthState.StoredCredentialsUsed; - - return { username: storedUsername, password: storedPassword }; - } - - // Find suitable window to show dialog: prefer to show it in the - // active window because any other network request will wait on - // the credentials and we want the user to present the dialog. - const window = this.windowsMainService.getFocusedWindow() || this.windowsMainService.getLastActiveWindow(); - if (!window) { - this.logService.trace('auth#doResolveProxyCredentials (proxy) - exit - no opened window found to show dialog in'); - - return undefined; // unexpected - } - - this.logService.trace(`auth#doResolveProxyCredentials (proxy) - asking window ${window.id} to handle proxy login`); - - // Open proxy dialog - const payload = { - authInfo, - username: this.sessionCredentials?.username ?? storedUsername, // prefer to show already used username (if any) over stored - password: this.sessionCredentials?.password ?? storedPassword, // prefer to show already used password (if any) over stored - replyChannel: `vscode:proxyAuthResponse:${generateUuid()}` - }; - window.sendWhenReady('vscode:openProxyAuthenticationDialog', CancellationToken.None, payload); - this.state = ProxyAuthState.LoginDialogShown; - - // Handle reply - const loginDialogCredentials = await new Promise(resolve => { - const proxyAuthResponseHandler = async (event: ElectronEvent, channel: string, reply: Credentials & { remember: boolean } | undefined /* canceled */) => { - if (channel === payload.replyChannel) { - this.logService.trace(`auth#doResolveProxyCredentials - exit - received credentials from window ${window.id}`); - window.win.webContents.off('ipc-message', proxyAuthResponseHandler); - - // We got credentials from the window - if (reply) { - const credentials: Credentials = { username: reply.username, password: reply.password }; - - // Update stored credentials based on `remember` flag - try { - if (reply.remember) { - const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.nativeHostMainService.setPassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); - } else { - await this.nativeHostMainService.deletePassword(undefined, ProxyAuthHandler2.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - } - } catch (error) { - this.logService.error(error); // handle gracefully - } - - resolve({ username: credentials.username, password: credentials.password }); - } - - // We did not get any credentials from the window (e.g. cancelled) - else { - resolve(undefined); - } - } - }; - - window.win.webContents.on('ipc-message', proxyAuthResponseHandler); - }); - - // Remember credentials for the session in case - // the credentials are wrong and we show the dialog - // again - this.sessionCredentials = loginDialogCredentials; - - return loginDialogCredentials; - } -} diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 783ac6fb565..38464e58e5c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,7 +5,8 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import * as fs from 'fs'; +import { unlinkSync } from 'fs'; +import { localize } from 'vs/nls'; import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; @@ -29,7 +30,6 @@ import { ConfigurationService } from 'vs/platform/configuration/common/configura import { IRequestService } from 'vs/platform/request/common/request'; import { RequestMainService } from 'vs/platform/request/electron-main/requestMainService'; import { CodeApplication } from 'vs/code/electron-main/app'; -import { localize } from 'vs/nls'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; @@ -256,7 +256,7 @@ class CodeMain { // let's delete it, since we can't connect to it and then // retry the whole thing try { - fs.unlinkSync(environmentService.mainIPCHandle); + unlinkSync(environmentService.mainIPCHandle); } catch (error) { logService.warn('Could not delete obsolete instance handle', error); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index dd721db92ad..1329bd72590 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -17,12 +17,11 @@ import { FileAccess } from 'vs/base/common/network'; export class SharedProcess implements ISharedProcess { - private barrier = new Barrier(); + private readonly barrier = new Barrier(); + private readonly _whenReady: Promise; private window: BrowserWindow | null = null; - private readonly _whenReady: Promise; - constructor( private readonly machineId: string, private userEnv: NodeJS.ProcessEnv, @@ -52,6 +51,7 @@ export class SharedProcess implements ISharedProcess { disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); + const config = { appRoot: this.environmentService.appRoot, machineId: this.machineId, @@ -110,7 +110,8 @@ export class SharedProcess implements ISharedProcess { }, 0); }); - return new Promise(c => { + return new Promise(resolve => { + // send payload once shared process is ready to receive it disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { sender.send('vscode:electron-main->shared-process=payload', { @@ -125,7 +126,7 @@ export class SharedProcess implements ISharedProcess { disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit'))); // complete IPC-ready promise when shared process signals this to us - ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined)); + ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => resolve(undefined)); })); }); } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index a9f47be8153..4379b1a8d6e 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import * as perf from 'vs/base/common/performance'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, Details } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, RenderProcessGoneDetails } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -28,7 +28,7 @@ import { resolveMarketplaceHeaders } from 'vs/platform/extensionManagement/commo import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; @@ -544,8 +544,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private onWindowError(error: WindowError.UNRESPONSIVE): void; - private onWindowError(error: WindowError.CRASHED, details: Details): void; - private onWindowError(error: WindowError, details?: Details): void { + private onWindowError(error: WindowError.CRASHED, details: RenderProcessGoneDetails): void; + private onWindowError(error: WindowError, details?: RenderProcessGoneDetails): void { this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not @@ -648,6 +648,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.currentMenuBarVisibility = newMenuBarVisibility; this.setMenuBarVisibility(newMenuBarVisibility); } + // Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options: const env = process.env; let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() @@ -780,6 +781,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.windowId = this._win.id; windowConfiguration.sessionId = `window:${this._win.id}`; windowConfiguration.logLevel = this.logService.getLevel(); + windowConfiguration.logsPath = this.environmentService.logsPath; // Set zoomlevel const windowConfig = this.configurationService.getValue('window'); diff --git a/src/vs/code/electron-sandbox/proxy/auth.html b/src/vs/code/electron-sandbox/proxy/auth.html deleted file mode 100644 index 788b68fce72..00000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - -

-
-

-
-

-

-

- - -

-
-
- - - - - diff --git a/src/vs/code/electron-sandbox/proxy/auth.js b/src/vs/code/electron-sandbox/proxy/auth.js deleted file mode 100644 index 5e0db3c2dc7..00000000000 --- a/src/vs/code/electron-sandbox/proxy/auth.js +++ /dev/null @@ -1,48 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const { ipcRenderer } = window.vscode; - -function promptForCredentials(data) { - return new Promise((c, e) => { - const $title = document.getElementById('title'); - const $username = document.getElementById('username'); - const $password = document.getElementById('password'); - const $form = document.getElementById('form'); - const $cancel = document.getElementById('cancel'); - const $message = document.getElementById('message'); - - function submit() { - c({ username: $username.value, password: $password.value }); - return false; - } - - function cancel() { - c({ username: '', password: '' }); - return false; - } - - $form.addEventListener('submit', submit); - $cancel.addEventListener('click', cancel); - - document.body.addEventListener('keydown', function (e) { - switch (e.keyCode) { - case 27: e.preventDefault(); e.stopPropagation(); return cancel(); - case 13: e.preventDefault(); e.stopPropagation(); return submit(); - } - }); - - $title.textContent = data.title; - $message.textContent = data.message; - $username.focus(); - }); -} - -ipcRenderer.on('vscode:openProxyAuthDialog', async (event, data) => { - const response = await promptForCredentials(data); - ipcRenderer.send('vscode:proxyAuthResponse', response); -}); diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 07eadee8415..2f26618aa4b 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -276,6 +276,7 @@ export class TextAreaHandler extends ViewPart { this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`); this._viewController.compositionStart(); + this._context.model.onCompositionStart(); })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { @@ -297,6 +298,7 @@ export class TextAreaHandler extends ViewPart { this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); this._viewController.compositionEnd(); + this._context.model.onCompositionEnd(); })); this._register(this._textAreaInput.onFocus(() => { diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 13552a60ec0..43305c283eb 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -684,10 +684,15 @@ export interface ICodeEditor extends editorCommon.IEditor { executeCommand(source: string | null | undefined, command: editorCommon.ICommand): void; /** - * Push an "undo stop" in the undo-redo stack. + * Create an "undo stop" in the undo-redo stack. */ pushUndoStop(): boolean; + /** + * Remove the "undo stop" in the undo-redo stack. + */ + popUndoStop(): boolean; + /** * Execute edits on the editor. * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 6b0867d496c..a9e8b99dc6f 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -69,6 +69,7 @@ export interface IBulkEditOptions { progress?: IProgress; token?: CancellationToken; showPreview?: boolean; + suppressPreview?: boolean; label?: string; quotableLabel?: string; undoRedoSource?: UndoRedoSource; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 6001fb4f7c2..7ca33211a3c 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -237,6 +237,7 @@ class MinimapLayout { options: MinimapOptions, viewportStartLineNumber: number, viewportEndLineNumber: number, + viewportStartLineNumberVerticalOffset: number, viewportHeight: number, viewportContainsWhitespaceGaps: boolean, lineCount: number, @@ -331,7 +332,8 @@ class MinimapLayout { } const endLineNumber = Math.min(lineCount, startLineNumber + minimapLinesFitting - 1); - const sliderTopAligned = (scrollTop / lineHeight - startLineNumber + 1) * minimapLineHeight / pixelRatio; + const partialLine = (scrollTop - viewportStartLineNumberVerticalOffset) / lineHeight; + const sliderTopAligned = (viewportStartLineNumber - startLineNumber + partialLine) * minimapLineHeight / pixelRatio; return new MinimapLayout(scrollTop, scrollHeight, true, computedSliderRatio, sliderTopAligned, sliderHeight, startLineNumber, endLineNumber); } @@ -505,6 +507,7 @@ interface IMinimapRenderingContext { readonly viewportStartLineNumber: number; readonly viewportEndLineNumber: number; + readonly viewportStartLineNumberVerticalOffset: number; readonly scrollTop: number; readonly scrollLeft: number; @@ -891,6 +894,7 @@ export class Minimap extends ViewPart implements IMinimapModel { viewportStartLineNumber: viewportStartLineNumber, viewportEndLineNumber: viewportEndLineNumber, + viewportStartLineNumberVerticalOffset: ctx.getVerticalOffsetForLineNumber(viewportStartLineNumber), scrollTop: ctx.scrollTop, scrollLeft: ctx.scrollLeft, @@ -1344,6 +1348,7 @@ class InnerMinimap extends Disposable { this._model.options, renderingCtx.viewportStartLineNumber, renderingCtx.viewportEndLineNumber, + renderingCtx.viewportStartLineNumberVerticalOffset, renderingCtx.viewportHeight, renderingCtx.viewportContainsWhitespaceGaps, this._model.getLineCount(), diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index ef3f6e301b4..ec5077ea0c7 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -25,6 +25,7 @@ export class ViewCursors extends ViewPart { private _cursorStyle: TextEditorCursorStyle; private _cursorSmoothCaretAnimation: boolean; private _selectionIsEmpty: boolean; + private _isComposingInput: boolean; private _isVisible: boolean; @@ -49,6 +50,7 @@ export class ViewCursors extends ViewPart { this._cursorStyle = options.get(EditorOption.cursorStyle); this._cursorSmoothCaretAnimation = options.get(EditorOption.cursorSmoothCaretAnimation); this._selectionIsEmpty = true; + this._isComposingInput = false; this._isVisible = false; @@ -83,7 +85,16 @@ export class ViewCursors extends ViewPart { } // --- begin event handlers - + public onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { + this._isComposingInput = true; + this._updateBlinking(); + return true; + } + public onCompositionEnd(e: viewEvents.ViewCompositionEndEvent): boolean { + this._isComposingInput = false; + this._updateBlinking(); + return true; + } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { const options = this._context.configuration.options; @@ -195,6 +206,10 @@ export class ViewCursors extends ViewPart { // ---- blinking logic private _getCursorBlinking(): TextEditorCursorBlinkingStyle { + if (this._isComposingInput) { + // avoid double cursors + return TextEditorCursorBlinkingStyle.Hidden; + } if (!this._editorHasFocus) { return TextEditorCursorBlinkingStyle.Hidden; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 2e5c5f443ae..4c87ce2bb2d 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -21,7 +21,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { IContentWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view/viewImpl'; import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; -import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations, InDiffEditorState } from 'vs/editor/common/config/editorOptions'; +import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -1122,6 +1122,18 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return true; } + public popUndoStop(): boolean { + if (!this._modelData) { + return false; + } + if (this._configuration.options.get(EditorOption.readOnly)) { + // read only editor => sorry! + return false; + } + this._modelData.model.popStackElement(); + return true; + } + public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { if (!this._modelData) { return false; @@ -1774,7 +1786,7 @@ class EditorContextKeysManager extends Disposable { this._editorTabMovesFocus.set(options.get(EditorOption.tabFocusMode)); this._editorReadonly.set(options.get(EditorOption.readOnly)); - this._inDiffEditor.set(options.get(EditorOption.inDiffEditor) !== InDiffEditorState.None); + this._inDiffEditor.set(options.get(EditorOption.inDiffEditor)); this._editorColumnSelection.set(options.get(EditorOption.columnSelection)); } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index c7ac517ae07..0730e4f00ec 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -20,7 +20,7 @@ import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { IDiffEditorOptions, IEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption, InDiffEditorState } from 'vs/editor/common/config/editorOptions'; +import { IDiffEditorOptions, IEditorOptions, EditorLayoutInfo, EditorOption, EditorOptions, EditorFontLigatures, stringSet as validateStringSetOption, boolean as validateBooleanOption } 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'; @@ -155,8 +155,8 @@ class VisualEditorState { let DIFF_EDITOR_ID = 0; -const diffInsertIcon = registerIcon('diff-insert', Codicon.add, nls.localize('diffInsertIcon', 'Line decoration for inserts in the diff editor')); -const diffRemoveIcon = registerIcon('diff-remove', Codicon.remove, nls.localize('diffRemoveIcon', 'Line decoration for removals in the diff editor')); +const diffInsertIcon = registerIcon('diff-insert', Codicon.add, nls.localize('diffInsertIcon', 'Line decoration for inserts in the diff editor.')); +const diffRemoveIcon = registerIcon('diff-remove', Codicon.remove, nls.localize('diffRemoveIcon', 'Line decoration for removals in the diff editor.')); const ttPolicy = window.trustedTypes?.createPolicy('diffEditorWidget', { createHTML: value => value }); export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffEditor { @@ -212,8 +212,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _maxComputationTime: number; private _renderIndicators: boolean; private _enableSplitViewResizing: boolean; - private _wordWrap: 'off' | 'on' | 'wordWrapColumn' | 'bounded' | undefined; - private _wordWrapMinified: boolean | undefined; private _strategy!: DiffEditorWidgetStyle; private readonly _updateDecorationsRunner: RunOnceScheduler; @@ -255,9 +253,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._domElement = domElement; options = options || {}; - this._wordWrap = options.wordWrap; - this._wordWrapMinified = options.wordWrapMinified; - // renderSideBySide this._renderSideBySide = true; if (typeof options.renderSideBySide !== 'undefined') { @@ -685,9 +680,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE public updateOptions(newOptions: IDiffEditorOptions): void { - this._wordWrap = typeof newOptions.wordWrap !== 'undefined' ? newOptions.wordWrap : this._wordWrap; - this._wordWrapMinified = typeof newOptions.wordWrapMinified !== 'undefined' ? newOptions.wordWrapMinified : this._wordWrapMinified; - // Handle side by side let renderSideBySideChanged = false; if (typeof newOptions.renderSideBySide !== 'undefined') { @@ -1108,6 +1100,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { const clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); + clonedOptions.inDiffEditor = true; clonedOptions.automaticLayout = false; clonedOptions.scrollbar = clonedOptions.scrollbar || {}; clonedOptions.scrollbar.vertical = 'visible'; @@ -1125,17 +1118,11 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); - result.inDiffEditor = (this._renderSideBySide ? InDiffEditorState.SideBySideLeft : InDiffEditorState.InlineLeft); if (!this._renderSideBySide) { - // do not wrap hidden editor - result.wordWrap = 'off'; - result.wordWrapMinified = false; - } else if (this._diffWordWrap === 'inherit') { - result.wordWrap = this._wordWrap; - result.wordWrapMinified = this._wordWrapMinified; + // never wrap hidden editor + result.wordWrapOverride1 = 'off'; } else { - result.wordWrap = this._diffWordWrap; - result.wordWrapMinified = this._wordWrapMinified; + result.wordWrapOverride1 = this._diffWordWrap; } result.readOnly = !this._originalIsEditable; result.extraEditorClassName = 'original-in-monaco-diff-editor'; @@ -1144,12 +1131,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IEditorConstructionOptions { const result = this._adjustOptionsForSubEditor(options); - result.inDiffEditor = (this._renderSideBySide ? InDiffEditorState.SideBySideRight : InDiffEditorState.InlineRight); - if (this._diffWordWrap === 'inherit') { - result.wordWrap = this._wordWrap; - } else { - result.wordWrap = this._diffWordWrap; - } + result.wordWrapOverride1 = this._diffWordWrap; result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 98cfa68d5dd..1c6b481c702 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -41,14 +41,6 @@ export const enum EditorAutoIndentStrategy { Full = 4 } -export const enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 -} - /** * Configuration options for the editor. */ @@ -56,7 +48,7 @@ export interface IEditorOptions { /** * This editor is used inside a diff editor. */ - inDiffEditor?: InDiffEditorState; + inDiffEditor?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -272,6 +264,14 @@ export interface IEditorOptions { * Defaults to "off". */ wordWrap?: 'off' | 'on' | 'wordWrapColumn' | 'bounded'; + /** + * Override the `wordWrap` setting. + */ + wordWrapOverride1?: 'off' | 'on' | 'inherit'; + /** + * Override the `wordWrapOverride1` setting. + */ + wordWrapOverride2?: 'off' | 'on' | 'inherit'; /** * Control the wrapping of the editor. * When `wordWrap` = "off", the lines will never wrap. @@ -281,11 +281,6 @@ export interface IEditorOptions { * Defaults to 80. */ wordWrapColumn?: number; - /** - * Force word wrapping when the text appears to be of a minified/generated file. - * Defaults to true. - */ - wordWrapMinified?: boolean; /** * Control indentation of wrapped lines. Can be: 'none', 'same', 'indent' or 'deepIndent'. * Defaults to 'same' in vscode and to 'none' in monaco-editor. @@ -1967,8 +1962,8 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption= 0) { @@ -367,7 +360,7 @@ export class TypeOperations { } if (keepPosition) { - return new ReplaceCommandWithoutChangingPosition(range, beforeText + config.normalizeIndentation(ir.afterEnter), true); + return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true); } else { let offset = 0; if (oldEndColumn <= firstNonWhitespace + 1) { @@ -376,7 +369,7 @@ export class TypeOperations { } offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0); } - return new ReplaceCommandWithOffsetCursorState(range, beforeText + config.normalizeIndentation(ir.afterEnter), 0, offset, true); + return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true); } } } diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 98ce23d7b0e..8f4d53a8447 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -855,7 +855,12 @@ export interface ITextModel { /** * @internal */ - hasSemanticTokens(): boolean; + hasCompleteSemanticTokens(): boolean; + + /** + * @internal + */ + hasSomeSemanticTokens(): boolean; /** * Flush all tokenization state. @@ -1094,12 +1099,17 @@ export interface ITextModel { detectIndentation(defaultInsertSpaces: boolean, defaultTabSize: number): void; /** - * Push a stack element onto the undo stack. This acts as an undo/redo point. - * The idea is to use `pushEditOperations` to edit the model and then to - * `pushStackElement` to create an undo/redo stop point. + * Close the current undo-redo element. + * This offers a way to create an undo/redo stop point. */ pushStackElement(): void; + /** + * Open the current undo-redo element. + * This offers a way to remove the current undo/redo stop point. + */ + popStackElement(): void; + /** * Push edit operations, basically editing the model. This is the preferred way * of editing the model. The edit operations will land on the undo stack. @@ -1143,7 +1153,7 @@ export interface ITextModel { _applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void; /** - * Undo edit operations until the first previous stop point created by `pushStackElement`. + * Undo edit operations until the previous undo/redo point. * The inverse edit operations will be pushed on the redo stack. * @internal */ @@ -1156,7 +1166,7 @@ export interface ITextModel { canUndo(): boolean; /** - * Redo edit operations until the next stop point created by `pushStackElement`. + * Redo edit operations until the next undo/redo point. * The inverse edit operations will be pushed on the undo stack. * @internal */ diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 0928d7d0e9b..d06f447b05e 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -199,6 +199,12 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement { } } + public open(): void { + if (!(this._data instanceof SingleModelEditStackData)) { + this._data = SingleModelEditStackData.deserialize(this._data); + } + } + public undo(): void { if (URI.isUri(this.model)) { // don't have a model @@ -315,6 +321,10 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { this._isOpen = false; } + public open(): void { + // cannot reopen + } + public undo(): void { this._isOpen = false; @@ -386,6 +396,13 @@ export class EditStack { } } + public popStackElement(): void { + const lastElement = this._undoRedoService.getLastElement(this._model.uri); + if (isEditStackElement(lastElement)) { + lastElement.open(); + } + } + public clear(): void { this._undoRedoService.removeElements(this._model.uri); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 2ba7cb4ee89..35dd466403a 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -1225,6 +1225,10 @@ export class TextModel extends Disposable implements model.ITextModel { this._commandManager.pushStackElement(); } + public popStackElement(): void { + this._commandManager.popStackElement(); + } + public pushEOL(eol: model.EndOfLineSequence): void { const currentEOL = (this.getEOL() === '\n' ? model.EndOfLineSequence.LF : model.EndOfLineSequence.CRLF); if (currentEOL === eol) { @@ -1880,12 +1884,16 @@ export class TextModel extends Disposable implements model.ITextModel { }); } - public hasSemanticTokens(): boolean { + public hasCompleteSemanticTokens(): boolean { return this._tokens2.isComplete(); } + public hasSomeSemanticTokens(): boolean { + return !this._tokens2.isEmpty(); + } + public setPartialSemanticTokens(range: Range, tokens: MultilineTokens2[]): void { - if (this.hasSemanticTokens()) { + if (this.hasCompleteSemanticTokens()) { return; } const changedRange = this._tokens2.setPartial(range, tokens); diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index a49ef27a71e..bbec0e36959 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -884,6 +884,10 @@ export class TokensStore2 { this._isComplete = false; } + public isEmpty(): boolean { + return (this._pieces.length === 0); + } + public set(pieces: MultilineTokens2[] | null, isComplete: boolean): void { this._pieces = pieces || []; this._isComplete = isComplete; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 7836fa5de72..8667276868a 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -790,6 +790,10 @@ class ModelSemanticColoring extends Disposable { } const provider = this._getSemanticColoringProvider(); if (!provider) { + if (this._currentDocumentResponse) { + // there are semantic tokens set + this._model.setSemanticTokens(null, false); + } return; } this._currentDocumentRequestCancellationTokenSource = new CancellationTokenSource(); diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 29659dfddd4..8c772e3c396 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -282,15 +282,16 @@ export enum EditorOption { wordWrapBreakAfterCharacters = 112, wordWrapBreakBeforeCharacters = 113, wordWrapColumn = 114, - wordWrapMinified = 115, - wrappingIndent = 116, - wrappingStrategy = 117, - showDeprecated = 118, - editorClassName = 119, - pixelRatio = 120, - tabFocusMode = 121, - layoutInfo = 122, - wrappingInfo = 123 + wordWrapOverride1 = 115, + wordWrapOverride2 = 116, + wrappingIndent = 117, + wrappingStrategy = 118, + showDeprecated = 119, + editorClassName = 120, + pixelRatio = 121, + tabFocusMode = 122, + layoutInfo = 123, + wrappingInfo = 124 } /** @@ -325,14 +326,6 @@ export enum EndOfLineSequence { CRLF = 1 } -export enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 -} - /** * Describes what to do with the indentation when pressing Enter. */ diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 804d1b977f5..bcdc69bb34f 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -11,6 +11,8 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { + ViewCompositionStart, + ViewCompositionEnd, ViewConfigurationChanged, ViewCursorStateChanged, ViewDecorationsChanged, @@ -29,6 +31,16 @@ export const enum ViewEventType { ViewZonesChanged, } +export class ViewCompositionStartEvent { + public readonly type = ViewEventType.ViewCompositionStart; + constructor() { } +} + +export class ViewCompositionEndEvent { + public readonly type = ViewEventType.ViewCompositionEnd; + constructor() { } +} + export class ViewConfigurationChangedEvent { public readonly type = ViewEventType.ViewConfigurationChanged; @@ -285,7 +297,9 @@ export class ViewZonesChangedEvent { } export type ViewEvent = ( - ViewConfigurationChangedEvent + ViewCompositionStartEvent + | ViewCompositionEndEvent + | ViewConfigurationChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index aad7a0b4957..e73ba9679b8 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -33,10 +33,15 @@ export class ViewEventHandler extends Disposable { // --- begin event handlers + public onCompositionStart(e: viewEvents.ViewCompositionStartEvent): boolean { + return false; + } + public onCompositionEnd(e: viewEvents.ViewCompositionEndEvent): boolean { + return false; + } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return false; } - public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return false; } @@ -94,6 +99,18 @@ export class ViewEventHandler extends Disposable { switch (e.type) { + case viewEvents.ViewEventType.ViewCompositionStart: + if (this.onCompositionStart(e)) { + shouldRender = true; + } + break; + + case viewEvents.ViewEventType.ViewCompositionEnd: + if (this.onCompositionEnd(e)) { + shouldRender = true; + } + break; + case viewEvents.ViewEventType.ViewConfigurationChanged: if (this.onConfigurationChanged(e)) { shouldRender = true; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index c69c16ea48c..125cb5880ae 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -164,6 +164,8 @@ export interface IViewModel extends ICursorSimpleModel { setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; tokenizeViewport(): void; setHasFocus(hasFocus: boolean): void; + onCompositionStart(): void; + onCompositionEnd(): void; onDidColorThemeChange(): void; getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[]; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 316e4f43e1b..932f6917e55 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -183,6 +183,14 @@ export class ViewModel extends Disposable implements IViewModel { this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus)); } + public onCompositionStart(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionStartEvent()); + } + + public onCompositionEnd(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewCompositionEndEvent()); + } + public onDidColorThemeChange(): void { this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewThemeChangedEvent()); } diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 9b3102c61b0..3fddfdff3c1 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -103,7 +103,7 @@ export class CodeLensContribution implements IEditorContribution { .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${codeLensHeight}px; font-size: ${fontSize}px; } `; if (fontFamily) { - newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: ${fontFamily}}`; + newStyle += `.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: '${fontFamily}'}`; } this._styleElement.textContent = newStyle; diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index 33944824331..597af6c3271 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -380,7 +380,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL } private _delayedUpdateHistory() { - this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)); + this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)).then(undefined, onUnexpectedError); } private _updateHistory() { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 8b1f88e7b39..57f95fe179f 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -8,7 +8,7 @@ import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { IMarkdownString, MarkdownString, isEmptyMarkdownString, markedStringsEquals } from 'vs/base/common/htmlContent'; -import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, DisposableStore, combinedDisposable, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -31,7 +31,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener'; import { MarkerController, NextMarkerAction } from 'vs/editor/contrib/gotoError/gotoError'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, createCancelablePromise, disposableTimeout } from 'vs/base/common/async'; import { getCodeActions, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction'; import { QuickFixAction, QuickFixController } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind, CodeActionTrigger } from 'vs/editor/contrib/codeAction/types'; @@ -578,6 +578,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { return hoverElement; } + private recentMarkerCodeActionsInfo: { marker: IMarker, hasCodeActions: boolean } | undefined = undefined; private renderMarkerStatusbar(markerHover: MarkerHover): HTMLElement { const hoverElement = $('div.hover-row.status-bar'); const disposables = new DisposableStore(); @@ -596,24 +597,28 @@ export class ModesContentHoverWidget extends ContentHoverWidget { if (!this._editor.getOption(EditorOption.readOnly)) { const quickfixPlaceholderElement = dom.append(actionsElement, $('div')); - quickfixPlaceholderElement.style.opacity = '0'; - quickfixPlaceholderElement.style.transition = 'opacity 0.2s'; - setTimeout(() => quickfixPlaceholderElement.style.opacity = '1', 200); - quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."); - disposables.add(toDisposable(() => quickfixPlaceholderElement.remove())); - + if (this.recentMarkerCodeActionsInfo) { + if (IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker) === IMarkerData.makeKey(markerHover.marker)) { + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { + quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); + } + } else { + this.recentMarkerCodeActionsInfo = undefined; + } + } + const updatePlaceholderDisposable = this.recentMarkerCodeActionsInfo && !this.recentMarkerCodeActionsInfo.hasCodeActions ? Disposable.None : disposables.add(disposableTimeout(() => quickfixPlaceholderElement.textContent = nls.localize('checkingForQuickFixes', "Checking for quick fixes..."), 200)); const codeActionsPromise = this.getCodeActions(markerHover.marker); disposables.add(toDisposable(() => codeActionsPromise.cancel())); codeActionsPromise.then(actions => { - quickfixPlaceholderElement.style.transition = ''; - quickfixPlaceholderElement.style.opacity = '1'; + updatePlaceholderDisposable.dispose(); + this.recentMarkerCodeActionsInfo = { marker: markerHover.marker, hasCodeActions: actions.validActions.length > 0 }; - if (!actions.validActions.length) { + if (!this.recentMarkerCodeActionsInfo.hasCodeActions) { actions.dispose(); quickfixPlaceholderElement.textContent = nls.localize('noQuickFixes', "No quick fixes available"); return; } - quickfixPlaceholderElement.remove(); + quickfixPlaceholderElement.style.display = 'none'; let showing = false; disposables.add(toDisposable(() => { diff --git a/src/vs/editor/contrib/indentation/indentation.ts b/src/vs/editor/contrib/indentation/indentation.ts index dcdcadea52b..720c8e76916 100644 --- a/src/vs/editor/contrib/indentation/indentation.ts +++ b/src/vs/editor/contrib/indentation/indentation.ts @@ -472,7 +472,6 @@ export class AutoIndentOnPaste implements IEditorContribution { } const autoIndent = this.editor.getOption(EditorOption.autoIndent); const { tabSize, indentSize, insertSpaces } = model.getOptions(); - this.editor.pushUndoStop(); let textEdits: TextEdit[] = []; let indentConverter = { @@ -583,9 +582,12 @@ export class AutoIndentOnPaste implements IEditorContribution { } } - let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); - this.editor.executeCommand('autoIndentOnPaste', cmd); - this.editor.pushUndoStop(); + if (textEdits.length > 0) { + this.editor.pushUndoStop(); + let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!); + this.editor.executeCommand('autoIndentOnPaste', cmd); + this.editor.pushUndoStop(); + } } private shouldIgnoreLine(model: ITextModel, lineNumber: number): boolean { diff --git a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts index 9c984b987b7..441460812f4 100644 --- a/src/vs/editor/contrib/linkedEditing/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/linkedEditing.ts @@ -220,6 +220,7 @@ export class LinkedEditingContribution extends Disposable implements IEditorCont } try { + this._editor.popUndoStop(); this._ignoreChangeEvent = true; const prevEditOperationType = this._editor._getViewModel().getPrevEditOperationType(); this._editor.executeEdits('linkedEditing', edits); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 54b7a807b9c..1494fdab300 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -14,7 +14,7 @@ import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentW import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import * as modes from 'vs/editor/common/modes'; import { IModeService } from 'vs/editor/common/services/modeService'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { IMarkdownRenderResult, MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import * as nls from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -27,6 +27,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { assertIsDefined } from 'vs/base/common/types'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; const $ = dom.$; @@ -225,8 +226,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { if (typeof activeParameter.documentation === 'string') { documentation.textContent = activeParameter.documentation; } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(activeParameter.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(activeParameter.documentation); documentation.appendChild(renderedContents.element); } dom.append(this.domNodes.docs, $('p', {}, documentation)); @@ -237,8 +237,7 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } else if (typeof signature.documentation === 'string') { dom.append(this.domNodes.docs, $('p', {}, signature.documentation)); } else { - const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(signature.documentation)); - renderedContents.element.classList.add('markdown-docs'); + const renderedContents = this.renderMarkdownDocs(signature.documentation); dom.append(this.domNodes.docs, renderedContents.element); } @@ -265,6 +264,16 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { this.domNodes.scrollbar.scanDomNode(); } + private renderMarkdownDocs(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const renderedContents = this.renderDisposeables.add(this.markdownRenderer.render(markdown, { + asyncRenderCallback: () => { + this.domNodes?.scrollbar.scanDomNode(); + } + })); + renderedContents.element.classList.add('markdown-docs'); + return renderedContents; + } + private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean { if (activeParameter && typeof activeParameter.documentation === 'string' && assertIsDefined(activeParameter.documentation).length > 0) { return true; diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 8e939a5ac1f..3717b9aa9ac 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -14,6 +14,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { mock } from 'vs/base/test/common/mock'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; suite('Snippet Variables Resolver', function () { @@ -332,7 +333,7 @@ suite('Snippet Variables Resolver', function () { // workspace with config const workspaceConfigPath = URI.file('testWorkspace.code-workspace'); - workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath), workspaceConfigPath); + workspace = new Workspace('', toWorkspaceFolders([{ path: 'folderName' }], workspaceConfigPath, extUriBiasedIgnorePathCase), workspaceConfigPath); assertVariableResolve(resolver, 'WORKSPACE_NAME', 'testWorkspace'); if (!isWindows) { assertVariableResolve(resolver, 'WORKSPACE_FOLDER', '/'); diff --git a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts index 4acc3c1326a..26cdc77ae04 100644 --- a/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts +++ b/src/vs/editor/contrib/suggest/suggestWidgetRenderer.ts @@ -33,7 +33,7 @@ export const suggestMoreInfoIcon = registerIcon('suggest-more-info', Codicon.che const _completionItemColor = new class ColorExtractor { - private static _regexRelaxed = /(#([\da-f]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))/; + private static _regexRelaxed = /(#([\da-fA-F]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))/; private static _regexStrict = new RegExp(`^${ColorExtractor._regexRelaxed.source}$`, 'i'); extract(item: CompletionItem, out: string[]): boolean { diff --git a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts index 0a317fd6f3a..44ca583901c 100644 --- a/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts +++ b/src/vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens.ts @@ -92,14 +92,20 @@ class ViewportSemanticTokensContribution extends Disposable implements IEditorCo return; } const model = this._editor.getModel(); - if (model.hasSemanticTokens()) { + if (model.hasCompleteSemanticTokens()) { return; } if (!isSemanticColoringEnabled(model, this._themeService, this._configurationService)) { + if (model.hasSomeSemanticTokens()) { + model.setSemanticTokens(null, false); + } return; } const provider = ViewportSemanticTokensContribution._getSemanticColoringProvider(model); if (!provider) { + if (model.hasSomeSemanticTokens()) { + model.setSemanticTokens(null, false); + } return; } const styling = this._modelService.getSemanticTokensProviderStyling(provider); diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index 37b24d51243..6d5fc9c32e4 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -363,7 +363,6 @@ export function createMonacoEditorAPI(): typeof monaco.editor { EditorOption: standaloneEnums.EditorOption, EndOfLinePreference: standaloneEnums.EndOfLinePreference, EndOfLineSequence: standaloneEnums.EndOfLineSequence, - InDiffEditorState: standaloneEnums.InDiffEditorState, MinimapPosition: standaloneEnums.MinimapPosition, MouseTargetType: standaloneEnums.MouseTargetType, OverlayWidgetPositionPreference: standaloneEnums.OverlayWidgetPositionPreference, diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 9bb40fa3750..dbf225a6c12 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -75,9 +75,11 @@ export function setLanguageConfiguration(languageId: string, configuration: Lang */ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSupport { + private readonly _languageIdentifier: modes.LanguageIdentifier; private readonly _actual: EncodedTokensProvider; - constructor(actual: EncodedTokensProvider) { + constructor(languageIdentifier: modes.LanguageIdentifier, actual: EncodedTokensProvider) { + this._languageIdentifier = languageIdentifier; this._actual = actual; } @@ -86,6 +88,9 @@ export class EncodedTokenizationSupport2Adapter implements modes.ITokenizationSu } public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + if (typeof this._actual.tokenize === 'function') { + return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, <{ tokenize(line: string, state: modes.IState): ILineTokens; }>this._actual, line, state, offsetDelta); + } throw new Error('Not supported!'); } @@ -114,7 +119,7 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return this._actual.getInitialState(); } - private _toClassicTokens(tokens: IToken[], language: string, offsetDelta: number): Token[] { + private static _toClassicTokens(tokens: IToken[], language: string, offsetDelta: number): Token[] { let result: Token[] = []; let previousStartIndex: number = 0; for (let i = 0, len = tokens.length; i < len; i++) { @@ -137,9 +142,9 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return result; } - public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { - let actualResult = this._actual.tokenize(line, state); - let tokens = this._toClassicTokens(actualResult.tokens, this._languageIdentifier.language, offsetDelta); + public static adaptTokenize(language: string, actual: { tokenize(line: string, state: modes.IState): ILineTokens; }, line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + let actualResult = actual.tokenize(line, state); + let tokens = TokenizationSupport2Adapter._toClassicTokens(actualResult.tokens, language, offsetDelta); let endState: modes.IState; // try to save an object if possible @@ -152,6 +157,10 @@ export class TokenizationSupport2Adapter implements modes.ITokenizationSupport { return new TokenizationResult(tokens, endState); } + public tokenize(line: string, state: modes.IState, offsetDelta: number): TokenizationResult { + return TokenizationSupport2Adapter.adaptTokenize(this._languageIdentifier.language, this._actual, line, state, offsetDelta); + } + private _toBinaryTokens(tokens: IToken[], offsetDelta: number): Uint32Array { const languageId = this._languageIdentifier.id; const tokenTheme = this._standaloneThemeService.getColorTheme().tokenTheme; @@ -287,6 +296,10 @@ export interface EncodedTokensProvider { * Tokenize a line given the state at the beginning of the line. */ tokenizeEncoded(line: string, state: modes.IState): IEncodedLineTokens; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenize?(line: string, state: modes.IState): ILineTokens; } function isEncodedTokensProvider(provider: TokensProvider | EncodedTokensProvider): provider is EncodedTokensProvider { @@ -307,7 +320,7 @@ export function setTokensProvider(languageId: string, provider: TokensProvider | } const create = (provider: TokensProvider | EncodedTokensProvider) => { if (isEncodedTokensProvider(provider)) { - return new EncodedTokenizationSupport2Adapter(provider); + return new EncodedTokenizationSupport2Adapter(languageIdentifier!, provider); } else { return new TokenizationSupport2Adapter(StaticServices.standaloneThemeService.get(), languageIdentifier!, provider); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index ac2fa484062..9dc368f6998 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -2208,6 +2208,42 @@ suite('Editor Controller - Regression tests', () => { }); }); + test('issue #110376: multiple selections with wordwrap behave differently', () => { + // a single model line => 4 view lines + withTestCodeEditor([ + [ + 'just a sentence. just a ', + 'sentence. just a sentence.', + ].join('') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 25 }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 1, 1, 16), + new Selection(1, 18, 1, 33), + new Selection(1, 35, 1, 50), + ]); + + moveLeft(editor, viewModel); + assertCursor(viewModel, [ + new Selection(1, 1, 1, 1), + new Selection(1, 18, 1, 18), + new Selection(1, 35, 1, 35), + ]); + + viewModel.setSelections('test', [ + new Selection(1, 1, 1, 16), + new Selection(1, 18, 1, 33), + new Selection(1, 35, 1, 50), + ]); + + moveRight(editor, viewModel); + assertCursor(viewModel, [ + new Selection(1, 16, 1, 16), + new Selection(1, 33, 1, 33), + new Selection(1, 50, 1, 50), + ]); + }); + }); + test('issue #98320: Multi-Cursor, Wrap lines and cursorSelectRight ==> cursors out of sync', () => { // a single model line => 4 view lines withTestCodeEditor([ @@ -4133,6 +4169,18 @@ suite('Editor Controller - Indentation Rules', () => { model.dispose(); mode.dispose(); }); + + test('issue #111128: Multicursor `Enter` issue with indentation', () => { + const model = createTextModel(' let a, b, c;', { detectIndentation: false, insertSpaces: false, tabSize: 4 }, mode.getLanguageIdentifier()); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + editor.setSelections([ + new Selection(1, 11, 1, 11), + new Selection(1, 14, 1, 14), + ]); + viewModel.type('\n', 'keyboard'); + assert.equal(model.getValue(), ' let a,\n\t b,\n\t c;'); + }); + }); }); interface ICursorOpts { diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index 14a6de5fdca..910165a0ea9 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -79,7 +79,8 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.wordWrap, 'off'); options._write(EditorOption.wordWrapColumn, 80); - options._write(EditorOption.wordWrapMinified, true); + options._write(EditorOption.wordWrapOverride1, 'inherit'); + options._write(EditorOption.wordWrapOverride2, 'inherit'); options._write(EditorOption.accessibilitySupport, 'auto'); const actual = EditorLayoutInfoComputer.computeLayout(options, { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1c2d827105a..2cb6913f72a 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1890,11 +1890,15 @@ declare namespace monaco.editor { */ detectIndentation(defaultInsertSpaces: boolean, defaultTabSize: number): void; /** - * Push a stack element onto the undo stack. This acts as an undo/redo point. - * The idea is to use `pushEditOperations` to edit the model and then to - * `pushStackElement` to create an undo/redo stop point. + * Close the current undo-redo element. + * This offers a way to create an undo/redo stop point. */ pushStackElement(): void; + /** + * Open the current undo-redo element. + * This offers a way to remove the current undo/redo stop point. + */ + popStackElement(): void; /** * Push edit operations, basically editing the model. This is the preferred way * of editing the model. The edit operations will land on the undo stack. @@ -2588,14 +2592,6 @@ declare namespace monaco.editor { Full = 4 } - export enum InDiffEditorState { - None = 0, - SideBySideLeft = 1, - SideBySideRight = 2, - InlineLeft = 3, - InlineRight = 4 - } - /** * Configuration options for the editor. */ @@ -2603,7 +2599,7 @@ declare namespace monaco.editor { /** * This editor is used inside a diff editor. */ - inDiffEditor?: InDiffEditorState; + inDiffEditor?: boolean; /** * The aria label for the editor's textarea (when it is focused). */ @@ -2819,6 +2815,14 @@ declare namespace monaco.editor { * Defaults to "off". */ wordWrap?: 'off' | 'on' | 'wordWrapColumn' | 'bounded'; + /** + * Override the `wordWrap` setting. + */ + wordWrapOverride1?: 'off' | 'on' | 'inherit'; + /** + * Override the `wordWrapOverride1` setting. + */ + wordWrapOverride2?: 'off' | 'on' | 'inherit'; /** * Control the wrapping of the editor. * When `wordWrap` = "off", the lines will never wrap. @@ -2828,11 +2832,6 @@ declare namespace monaco.editor { * Defaults to 80. */ wordWrapColumn?: number; - /** - * Force word wrapping when the text appears to be of a minified/generated file. - * Defaults to true. - */ - wordWrapMinified?: boolean; /** * Control indentation of wrapped lines. Can be: 'none', 'same', 'indent' or 'deepIndent'. * Defaults to 'same' in vscode and to 'none' in monaco-editor. @@ -4019,15 +4018,16 @@ declare namespace monaco.editor { wordWrapBreakAfterCharacters = 112, wordWrapBreakBeforeCharacters = 113, wordWrapColumn = 114, - wordWrapMinified = 115, - wrappingIndent = 116, - wrappingStrategy = 117, - showDeprecated = 118, - editorClassName = 119, - pixelRatio = 120, - tabFocusMode = 121, - layoutInfo = 122, - wrappingInfo = 123 + wordWrapOverride1 = 115, + wordWrapOverride2 = 116, + wrappingIndent = 117, + wrappingStrategy = 118, + showDeprecated = 119, + editorClassName = 120, + pixelRatio = 121, + tabFocusMode = 122, + layoutInfo = 123, + wrappingInfo = 124 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4080,7 +4080,7 @@ declare namespace monaco.editor { hideCursorInOverviewRuler: IEditorOption; highlightActiveIndentGuide: IEditorOption; hover: IEditorOption; - inDiffEditor: IEditorOption; + inDiffEditor: IEditorOption; letterSpacing: IEditorOption; lightbulb: IEditorOption; lineDecorationsWidth: IEditorOption; @@ -4146,7 +4146,8 @@ declare namespace monaco.editor { wordWrapBreakAfterCharacters: IEditorOption; wordWrapBreakBeforeCharacters: IEditorOption; wordWrapColumn: IEditorOption; - wordWrapMinified: IEditorOption; + wordWrapOverride1: IEditorOption; + wordWrapOverride2: IEditorOption; wrappingIndent: IEditorOption; wrappingStrategy: IEditorOption; editorClassName: IEditorOption; @@ -4743,9 +4744,13 @@ declare namespace monaco.editor { */ executeCommand(source: string | null | undefined, command: ICommand): void; /** - * Push an "undo stop" in the undo-redo stack. + * Create an "undo stop" in the undo-redo stack. */ pushUndoStop(): boolean; + /** + * Remove the "undo stop" in the undo-redo stack. + */ + popUndoStop(): boolean; /** * Execute edits on the editor. * The edits will land on the undo-redo stack, but no "undo stop" will be pushed. @@ -5064,6 +5069,10 @@ declare namespace monaco.languages { * Tokenize a line given the state at the beginning of the line. */ tokenizeEncoded(line: string, state: IState): IEncodedLineTokens; + /** + * Tokenize a line given the state at the beginning of the line. + */ + tokenize?(line: string, state: IState): ILineTokens; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 9268e3d4ca4..1344ea90093 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -20,7 +20,8 @@ import { isWindows, isLinux } from 'vs/base/common/platform'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable { const groups = menu.getActions(options); - const useAlternativeActions = ModifierKeyEmitter.getInstance().keyStatus.altKey; + const modifierKeyEmitter = ModifierKeyEmitter.getInstance(); + const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey); fillInActions(groups, target, useAlternativeActions, isPrimaryGroup); return asDisposable(groups); } diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 01bef2897ce..ea7c83876ff 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -6,8 +6,9 @@ import { Event } from 'vs/base/common/event'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +import { userAgent, isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; +let _userAgent = userAgent || ''; const STATIC_VALUES = new Map(); STATIC_VALUES.set('false', false); STATIC_VALUES.set('true', true); @@ -16,6 +17,11 @@ STATIC_VALUES.set('isLinux', isLinux); STATIC_VALUES.set('isWindows', isWindows); STATIC_VALUES.set('isWeb', isWeb); STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); +STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0); +STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0); +STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0); +STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0); +STATIC_VALUES.set('isIPad', _userAgent.indexOf('iPad') >= 0); const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -32,6 +38,10 @@ export const enum ContextKeyExprType { Or = 9, In = 10, NotIn = 11, + Greater = 12, + GreaterEquals = 13, + Smaller = 14, + SmallerEquals = 15, } export interface IContextKeyExprMapper { @@ -39,6 +49,10 @@ export interface IContextKeyExprMapper { mapNot(key: string): ContextKeyExpression; mapEquals(key: string, value: any): ContextKeyExpression; mapNotEquals(key: string, value: any): ContextKeyExpression; + mapGreater(key: string, value: any): ContextKeyExpression; + mapGreaterEquals(key: string, value: any): ContextKeyExpression; + mapSmaller(key: string, value: any): ContextKeyExpression; + mapSmallerEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; mapIn(key: string, valueKey: string): ContextKeyInExpr; } @@ -57,7 +71,9 @@ export interface IContextKeyExpression { export type ContextKeyExpression = ( ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr - | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr + | ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr + | ContextKeySmallerExpr | ContextKeySmallerEqualsExpr ); export abstract class ContextKeyExpr { @@ -102,6 +118,14 @@ export abstract class ContextKeyExpr { return ContextKeyOrExpr.create(expr); } + public static greater(key: string, value: any): ContextKeyExpression { + return ContextKeyGreaterExpr.create(key, value); + } + + public static less(key: string, value: any): ContextKeyExpression { + return ContextKeySmallerExpr.create(key, value); + } + public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { if (!serialized) { return undefined; @@ -143,6 +167,26 @@ export abstract class ContextKeyExpr { return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim()); } + if (/^[^<=>]+>=[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('>='); + return ContextKeyGreaterEqualsExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+>[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('>'); + return ContextKeyGreaterExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+<=[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('<='); + return ContextKeySmallerEqualsExpr.create(pieces[0].trim(), pieces[1].trim()); + } + + if (/^[^<=>]+<[^<=>]+$/.test(serializedOne)) { + const pieces = serializedOne.split('<'); + return ContextKeySmallerExpr.create(pieces[0].trim(), pieces[1].trim()); + } + if (/^\!\s*/.test(serializedOne)) { return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); } @@ -302,13 +346,7 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; + return cmp1(this.key, other.key); } public equals(other: ContextKeyExpression): boolean { @@ -362,19 +400,7 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; + return cmp2(this.key, this.value, other.key, other.value); } public equals(other: ContextKeyExpression): boolean { @@ -422,19 +448,7 @@ export class ContextKeyInExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.valueKey < other.valueKey) { - return -1; - } - if (this.valueKey > other.valueKey) { - return 1; - } - return 0; + return cmp2(this.key, this.valueKey, other.key, other.valueKey); } public equals(other: ContextKeyExpression): boolean { @@ -549,19 +563,7 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - if (this.value < other.value) { - return -1; - } - if (this.value > other.value) { - return 1; - } - return 0; + return cmp2(this.key, this.value, other.key, other.value); } public equals(other: ContextKeyExpression): boolean { @@ -613,13 +615,7 @@ export class ContextKeyNotExpr implements IContextKeyExpression { if (other.type !== this.type) { return this.type - other.type; } - if (this.key < other.key) { - return -1; - } - if (this.key > other.key) { - return 1; - } - return 0; + return cmp1(this.key, other.key); } public equals(other: ContextKeyExpression): boolean { @@ -650,6 +646,200 @@ export class ContextKeyNotExpr implements IContextKeyExpression { } } +export class ContextKeyGreaterExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeyGreaterExpr(key, value); + } + + public readonly type = ContextKeyExprType.Greater; + + private constructor( + private readonly key: string, + private readonly value: any + ) { } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) > parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} > ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapGreater(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeySmallerEqualsExpr.create(this.key, this.value); + } +} + +export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeyGreaterEqualsExpr(key, value); + } + + public readonly type = ContextKeyExprType.GreaterEquals; + + private constructor( + private readonly key: string, + private readonly value: any + ) { } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) >= parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} >= ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapGreaterEquals(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeySmallerExpr.create(this.key, this.value); + } +} + +export class ContextKeySmallerExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeySmallerExpr(key, value); + } + + public readonly type = ContextKeyExprType.Smaller; + + private constructor( + private readonly key: string, + private readonly value: any + ) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) < parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} < ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapSmaller(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeyGreaterEqualsExpr.create(this.key, this.value); + } +} + +export class ContextKeySmallerEqualsExpr implements IContextKeyExpression { + + public static create(key: string, value: any): ContextKeyExpression { + return new ContextKeySmallerEqualsExpr(key, value); + } + + public readonly type = ContextKeyExprType.SmallerEquals; + + private constructor( + private readonly key: string, + private readonly value: any + ) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return cmp2(this.key, this.value, other.key, other.value); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.value === other.value); + } + return false; + } + + public evaluate(context: IContext): boolean { + return (parseFloat(context.getValue(this.key)) <= parseFloat(this.value)); + } + + public serialize(): string { + return `${this.key} <= ${this.value}`; + } + + public keys(): string[] { + return [this.key]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return mapFnc.mapSmallerEquals(this.key, this.value); + } + + public negate(): ContextKeyExpression { + return ContextKeyGreaterExpr.create(this.key, this.value); + } +} + export class ContextKeyRegexExpr implements IContextKeyExpression { public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr { @@ -1143,3 +1333,29 @@ export interface IContextKeyService { } export const SET_CONTEXT_COMMAND_ID = 'setContext'; + +function cmp1(key1: string, key2: string): number { + if (key1 < key2) { + return -1; + } + if (key1 > key2) { + return 1; + } + return 0; +} + +function cmp2(key1: string, value1: any, key2: string, value2: any): number { + if (key1 < key2) { + return -1; + } + if (key1 > key2) { + return 1; + } + if (value1 < value2) { + return -1; + } + if (value1 > value2) { + return 1; + } + return 0; +} diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 2912df4b0ea..91a548be66c 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -186,4 +186,84 @@ suite('ContextKeyExpr', () => { ); assert.equal(actual!.equals(expected!), true); }); + + test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => { + function checkEvaluate(expr: string, ctx: any, expected: any): void { + const _expr = ContextKeyExpr.deserialize(expr)!; + assert.equal(_expr.evaluate(createContext(ctx)), expected); + } + + checkEvaluate('a>1', {}, false); + checkEvaluate('a>1', { a: 0 }, false); + checkEvaluate('a>1', { a: 1 }, false); + checkEvaluate('a>1', { a: 2 }, true); + checkEvaluate('a>1', { a: '0' }, false); + checkEvaluate('a>1', { a: '1' }, false); + checkEvaluate('a>1', { a: '2' }, true); + checkEvaluate('a>1', { a: 'a' }, false); + + checkEvaluate('a>10', { a: 2 }, false); + checkEvaluate('a>10', { a: 11 }, true); + checkEvaluate('a>10', { a: '11' }, true); + checkEvaluate('a>10', { a: '2' }, false); + checkEvaluate('a>10', { a: '11' }, true); + + checkEvaluate('a>1.1', { a: 1 }, false); + checkEvaluate('a>1.1', { a: 2 }, true); + checkEvaluate('a>1.1', { a: 11 }, true); + checkEvaluate('a>1.1', { a: '1.1' }, false); + checkEvaluate('a>1.1', { a: '2' }, true); + checkEvaluate('a>1.1', { a: '11' }, true); + + checkEvaluate('a>b', { a: 'b' }, false); + checkEvaluate('a>b', { a: 'c' }, false); + checkEvaluate('a>b', { a: 1000 }, false); + + checkEvaluate('a >= 2', { a: '1' }, false); + checkEvaluate('a >= 2', { a: '2' }, true); + checkEvaluate('a >= 2', { a: '3' }, true); + + checkEvaluate('a < 2', { a: '1' }, true); + checkEvaluate('a < 2', { a: '2' }, false); + checkEvaluate('a < 2', { a: '3' }, false); + + checkEvaluate('a <= 2', { a: '1' }, true); + checkEvaluate('a <= 2', { a: '2' }, true); + checkEvaluate('a <= 2', { a: '3' }, false); + }); + + test('Greater, GreaterEquals, Smaller, SmallerEquals negate', () => { + function checkNegate(expr: string, expected: string): void { + const a = ContextKeyExpr.deserialize(expr)!; + const b = a.negate(); + assert.equal(b.serialize(), expected); + } + + checkNegate('a>1', 'a <= 1'); + checkNegate('a>1.1', 'a <= 1.1'); + checkNegate('a>b', 'a <= b'); + + checkNegate('a>=1', 'a < 1'); + checkNegate('a>=1.1', 'a < 1.1'); + checkNegate('a>=b', 'a < b'); + + checkNegate('a<1', 'a >= 1'); + checkNegate('a<1.1', 'a >= 1.1'); + checkNegate('a= b'); + + checkNegate('a<=1', 'a > 1'); + checkNegate('a<=1.1', 'a > 1.1'); + checkNegate('a<=b', 'a > b'); + }); + + test('issue #111899: context keys can use `<` or `>` ', () => { + const actual = ContextKeyExpr.deserialize('editorTextFocus && vim.active && vim.use')!; + assert.ok(actual.equals( + ContextKeyExpr.and( + ContextKeyExpr.has('editorTextFocus'), + ContextKeyExpr.has('vim.active'), + ContextKeyExpr.has('vim.use'), + )! + )); + }); }); diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index b263bdd9c1c..b30c4e44e73 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -5,7 +5,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { IProcessEnvironment } from 'vs/base/common/platform'; export const IExtensionHostDebugService = createDecorator('extensionHostDebugService'); @@ -16,11 +15,6 @@ export interface IAttachSessionEvent { port: number; } -export interface ILogToSessionEvent { - sessionId: string; - log: IRemoteConsoleLog; -} - export interface ITerminateSessionEvent { sessionId: string; subId?: string; @@ -50,9 +44,6 @@ export interface IExtensionHostDebugService { attachSession(sessionId: string, port: number, subId?: string): void; readonly onAttachSession: Event; - logToSession(sessionId: string, log: IRemoteConsoleLog): void; - readonly onLogToSession: Event; - terminateSession(sessionId: string, subId?: string): void; readonly onTerminateSession: Event; diff --git a/src/vs/platform/debug/common/extensionHostDebugIpc.ts b/src/vs/platform/debug/common/extensionHostDebugIpc.ts index 60011be13e3..09c2a1916fa 100644 --- a/src/vs/platform/debug/common/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/common/extensionHostDebugIpc.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { Event, Emitter } from 'vs/base/common/event'; -import { IRemoteConsoleLog } from 'vs/base/common/console'; import { Disposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -17,7 +16,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan private readonly _onCloseEmitter = new Emitter(); private readonly _onReloadEmitter = new Emitter(); private readonly _onTerminateEmitter = new Emitter(); - private readonly _onLogToEmitter = new Emitter(); private readonly _onAttachEmitter = new Emitter(); call(ctx: TContext, command: string, arg?: any): Promise { @@ -28,8 +26,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return Promise.resolve(this._onReloadEmitter.fire({ sessionId: arg[0] })); case 'terminate': return Promise.resolve(this._onTerminateEmitter.fire({ sessionId: arg[0] })); - case 'log': - return Promise.resolve(this._onLogToEmitter.fire({ sessionId: arg[0], log: arg[1] })); case 'attach': return Promise.resolve(this._onAttachEmitter.fire({ sessionId: arg[0], port: arg[1], subId: arg[2] })); } @@ -44,8 +40,6 @@ export class ExtensionHostDebugBroadcastChannel implements IServerChan return this._onReloadEmitter.event; case 'terminate': return this._onTerminateEmitter.event; - case 'log': - return this._onLogToEmitter.event; case 'attach': return this._onAttachEmitter.event; } @@ -85,14 +79,6 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte return this.channel.listen('attach'); } - logToSession(sessionId: string, log: IRemoteConsoleLog): void { - this.channel.call('log', [sessionId, log]); - } - - get onLogToSession(): Event { - return this.channel.listen('log'); - } - terminateSession(sessionId: string, subId?: string): void { this.channel.call('terminate', [sessionId, subId]); } diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index d05bd7ca0c4..7cc40f9086d 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -9,7 +9,7 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { createServer, AddressInfo } from 'net'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts similarity index 100% rename from src/vs/platform/dialogs/electron-main/dialogs.ts rename to src/vs/platform/dialogs/electron-main/dialogMainService.ts diff --git a/src/vs/platform/environment/test/node/nativeModules.test.ts b/src/vs/platform/environment/test/node/nativeModules.test.ts index b64282f626f..34617f575bb 100644 --- a/src/vs/platform/environment/test/node/nativeModules.test.ts +++ b/src/vs/platform/environment/test/node/nativeModules.test.ts @@ -90,7 +90,7 @@ suite('Native Modules (all platforms)', () => { test('vscode-windows-ca-certs', async () => { // @ts-ignore Windows only const windowsCerts = await import('vscode-windows-ca-certs'); - const store = windowsCerts(); + const store = new windowsCerts.Crypt32(); assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs')); let certCount = 0; try { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 3f0dd835cd3..45d3b68c483 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -9,7 +9,7 @@ import { getGalleryExtensionId, getGalleryExtensionTelemetryData, adoptToGallery import { getOrDefault } from 'vs/base/common/objects'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IPager } from 'vs/base/common/paging'; -import { IRequestService, asJson, asText } from 'vs/platform/request/common/request'; +import { IRequestService, asJson, asText, isSuccess } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -147,6 +147,35 @@ const DefaultQueryState: IQueryState = { assetTypes: [] }; +type GalleryServiceQueryClassification = { + filterTypes: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortBy: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + sortOrder: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + duration: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', 'isMeasurement': true }; + success: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + requestBodySize: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + responseBodySize?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + statusCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + errorCode?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; + count?: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +type QueryTelemetryData = { + filterTypes: string[]; + sortBy: string; + sortOrder: string; +}; + +type GalleryServiceQueryEvent = QueryTelemetryData & { + duration: number; + success: boolean; + requestBodySize: string; + responseBodySize?: string; + statusCode?: string; + errorCode?: string; + count?: string; +}; + class Query { constructor(private state = DefaultQueryState) { } @@ -196,6 +225,14 @@ class Query { const criterium = this.state.criteria.filter(criterium => criterium.filterType === FilterType.SearchText)[0]; return criterium && criterium.value ? criterium.value : ''; } + + get telemetryData(): QueryTelemetryData { + return { + filterTypes: this.state.criteria.map(criterium => String(criterium.filterType)), + sortBy: String(this.sortBy), + sortOrder: String(this.sortOrder) + }; + } } function getStatistic(statistics: IRawGalleryExtensionStatistics[], name: string): number { @@ -447,20 +484,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { throw new Error('No extension gallery service configured.'); } - const type = options.names ? 'ids' : (options.text ? 'text' : 'all'); let text = options.text || ''; const pageSize = getOrDefault(options, o => o.pageSize, 50); - type GalleryServiceQueryClassification = { - type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - text: { classification: 'CustomerContent', purpose: 'FeatureInsight' }; - }; - type GalleryServiceQueryEvent = { - type: string; - text: string; - }; - this.telemetryService.publicLog2('galleryService:query', { type, text }); - let query = new Query() .withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, pageSize) @@ -543,27 +569,49 @@ export class ExtensionGalleryService implements IExtensionGalleryService { 'Content-Length': String(data.length) }; - const context = await this.requestService.request({ - type: 'POST', - url: this.api('/extensionquery'), - data, - headers - }, token); + const startTime = new Date().getTime(); + let context: IRequestContext | undefined, error: any, total: number = 0; - if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { - return { galleryExtensions: [], total: 0 }; + try { + context = await this.requestService.request({ + type: 'POST', + url: this.api('/extensionquery'), + data, + headers + }, token); + + if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { + return { galleryExtensions: [], total }; + } + + const result = await asJson(context); + if (result) { + const r = result.results[0]; + const galleryExtensions = r.extensions; + const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; + total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; + + return { galleryExtensions, total }; + } + return { galleryExtensions: [], total }; + + } catch (e) { + error = e; + throw e; + } finally { + this.telemetryService.publicLog2('galleryService:query', { + ...query.telemetryData, + requestBodySize: String(data.length), + duration: new Date().getTime() - startTime, + success: !!context && isSuccess(context), + responseBodySize: context?.res.headers['Content-Length'], + statusCode: context ? String(context.res.statusCode) : undefined, + errorCode: error + ? isPromiseCanceledError(error) ? 'canceled' : getErrorMessage(error).startsWith('XHR timeout') ? 'timeout' : 'failed' + : undefined, + count: String(total) + }); } - - const result = await asJson(context); - if (result) { - const r = result.results[0]; - const galleryExtensions = r.extensions; - const resultCount = r.resultMetadata && r.resultMetadata.filter(m => m.metadataType === 'ResultCount')[0]; - const total = resultCount && resultCount.metadataItems.filter(i => i.name === 'TotalCount')[0].count || 0; - - return { galleryExtensions, total }; - } - return { galleryExtensions: [], total: 0 }; } async reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise { diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 23001b2a20f..81ee5fe2580 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -725,16 +725,16 @@ export class FileService extends Disposable implements IFileService { // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { - const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); + const { providerExtUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); if (!isPathCaseSensitive) { - isSameResourceWithDifferentPathCase = extUri.isEqual(source, target); + isSameResourceWithDifferentPathCase = providerExtUri.isEqual(source, target); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } - if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) { + if (!isSameResourceWithDifferentPathCase && providerExtUri.isEqualOrParent(target, source)) { throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -751,8 +751,8 @@ export class FileService extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { - const { extUri } = this.getExtUri(sourceProvider); - if (extUri.isEqualOrParent(source, target)) { + const { providerExtUri } = this.getExtUri(sourceProvider); + if (providerExtUri.isEqualOrParent(source, target)) { throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } @@ -761,11 +761,11 @@ export class FileService extends Disposable implements IFileService { return { exists, isSameResourceWithDifferentPathCase }; } - private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } { + private getExtUri(provider: IFileSystemProvider): { providerExtUri: IExtUri, isPathCaseSensitive: boolean } { const isPathCaseSensitive = this.isPathCaseSensitive(provider); return { - extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, + providerExtUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, isPathCaseSensitive }; } @@ -791,8 +791,8 @@ export class FileService extends Disposable implements IFileService { const directoriesToCreate: string[] = []; // mkdir until we reach root - const { extUri } = this.getExtUri(provider); - while (!extUri.isEqual(directory, dirname(directory))) { + const { providerExtUri } = this.getExtUri(provider); + while (!providerExtUri.isEqual(directory, dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { @@ -940,12 +940,12 @@ export class FileService extends Disposable implements IFileService { } private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string { - const { extUri } = this.getExtUri(provider); + const { providerExtUri } = this.getExtUri(provider); return [ - extUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive - String(options.recursive), // use recursive: true | false as part of the key - options.excludes.join() // use excludes as part of the key + providerExtUri.getComparisonKey(resource), // lowercase path if the provider is case insensitive + String(options.recursive), // use recursive: true | false as part of the key + options.excludes.join() // use excludes as part of the key ].join(); } @@ -963,8 +963,8 @@ export class FileService extends Disposable implements IFileService { private readonly writeQueues: Map> = new Map(); private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue { - const { extUri } = this.getExtUri(provider); - const queueKey = extUri.getComparisonKey(resource); + const { providerExtUri } = this.getExtUri(provider); + const queueKey = providerExtUri.getComparisonKey(resource); // ensure to never write to the same resource without finishing // the one write. this ensures a write finishes consistently diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 4ae15434563..784bd056d94 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -947,9 +947,9 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } -export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { +export async function whenProviderRegistered(file: URI, fileService: IFileService): Promise { if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { - return Promise.resolve(); + return; } return new Promise(resolve => { diff --git a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts index 51a4b1a6510..70c4cfd6193 100644 --- a/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts +++ b/src/vs/platform/files/node/watcher/nsfw/nsfwWatcherService.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as glob from 'vs/base/common/glob'; -import * as extpath from 'vs/base/common/extpath'; -import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; -import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import * as nsfw from 'vscode-nsfw'; +import * as glob from 'vs/base/common/glob'; +import { join } from 'vs/base/common/path'; +import { isMacintosh } from 'vs/base/common/platform'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { IDiskFileChange, normalizeFileChanges, ILogMessage } from 'vs/platform/files/node/watcher/watcher'; import { IWatcherService, IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher'; import { ThrottledDelayer } from 'vs/base/common/async'; import { FileChangeType } from 'vs/platform/files/common/files'; @@ -111,7 +111,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // We have to detect this case and massage the events to correct this. let realBasePathDiffers = false; let realBasePathLength = request.path.length; - if (platform.isMacintosh) { + if (isMacintosh) { try { // First check for symbolic link @@ -141,7 +141,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { for (const e of events) { // Logging if (this.verboseLogging) { - const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || ''); + const logPath = e.action === nsfw.actions.RENAMED ? join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : join(e.directory, e.file || ''); this.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`); } @@ -149,20 +149,20 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { let absolutePath: string; if (e.action === nsfw.actions.RENAMED) { // Rename fires when a file's name changes within a single directory - absolutePath = path.join(e.directory, e.oldFile || ''); + absolutePath = join(e.directory, e.oldFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } - absolutePath = path.join(e.newDirectory || e.directory, e.newFile || ''); + absolutePath = join(e.newDirectory || e.directory, e.newFile || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath }); } else if (this.verboseLogging) { this.log(` >> ignored ${absolutePath}`); } } else { - absolutePath = path.join(e.directory, e.file || ''); + absolutePath = join(e.directory, e.file || ''); if (!this.isPathIgnored(absolutePath, this.pathWatchers[request.path].ignored)) { undeliveredFileEvents.push({ type: nsfwActionToRawChangeType[e.action], @@ -179,7 +179,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { const events = undeliveredFileEvents; undeliveredFileEvents = []; - if (platform.isMacintosh) { + if (isMacintosh) { events.forEach(e => { // Mac uses NFD unicode form on disk, but we want NFC @@ -230,7 +230,7 @@ export class NsfwWatcherService extends Disposable implements IWatcherService { // Normalizes a set of root paths by removing any root paths that are // sub-paths of other roots. return roots.filter(r => roots.every(other => { - return !(r.path.length > other.path.length && extpath.isEqualOrParent(r.path, other.path)); + return !(r.path.length > other.path.length && isEqualOrParent(r.path, other.path)); })); } diff --git a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts index 895e5dfa959..25276aa2ac4 100644 --- a/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/platform/files/node/watcher/unix/chokidarWatcherService.ts @@ -6,9 +6,8 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); -import * as extpath from 'vs/base/common/extpath'; import * as glob from 'vs/base/common/glob'; +import { isEqualOrParent } from 'vs/base/common/extpath'; import { FileChangeType } from 'vs/platform/files/common/files'; import { ThrottledDelayer } from 'vs/base/common/async'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -20,6 +19,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { equals } from 'vs/base/common/arrays'; import { Disposable } from 'vs/base/common/lifecycle'; +gracefulFs.gracefulify(fs); // enable gracefulFs + process.noAsar = true; // disable ASAR support in watcher process interface IWatcher { @@ -311,7 +312,7 @@ function isIgnored(path: string, requests: ExtendedWatcherRequest[]): boolean { return false; } - if (extpath.isEqualOrParent(path, request.path)) { + if (isEqualOrParent(path, request.path)) { if (!request.parsedPattern) { if (request.excludes && request.excludes.length > 0) { const pattern = `{${request.excludes.join(',')}}`; @@ -343,7 +344,7 @@ export function normalizeRoots(requests: IWatcherRequest[]): { [basePath: string for (const request of requests) { const basePath = request.path; const ignored = (request.excludes || []).sort(); - if (prevRequest && (extpath.isEqualOrParent(basePath, prevRequest.path))) { + if (prevRequest && (isEqualOrParent(basePath, prevRequest.path))) { if (!isEqualIgnore(ignored, prevRequest.excludes)) { result[prevRequest.path].push({ path: basePath, excludes: ignored }); } diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index da98e2f94f6..066d2193bd2 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -17,7 +17,7 @@ import { isMacintosh, IProcessEnvironment } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; import { FileAccess } from 'vs/base/common/network'; @@ -185,120 +185,116 @@ export class IssueMainService implements ICommonIssueService { } } - openReporter(data: IssueReporterData): Promise { - return new Promise(_ => { - if (!this._issueWindow) { - this._issueParentWindow = BrowserWindow.getFocusedWindow(); - if (this._issueParentWindow) { - const position = this.getWindowPosition(this._issueParentWindow, 700, 800); + async openReporter(data: IssueReporterData): Promise { + if (!this._issueWindow) { + this._issueParentWindow = BrowserWindow.getFocusedWindow(); + if (this._issueParentWindow) { + const position = this.getWindowPosition(this._issueParentWindow, 700, 800); - this._issueWindow = new BrowserWindow({ - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - title: localize('issueReporter', "Issue Reporter"), - backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + this._issueWindow = new BrowserWindow({ + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + title: localize('issueReporter', "Issue Reporter"), + backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented + this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented - // Modified when testing UI - const features: IssueReporterFeatures = {}; + // Modified when testing UI + const features: IssueReporterFeatures = {}; - this.logService.trace('issueService#openReporter: opening issue reporter'); - this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); + this.logService.trace('issueService#openReporter: opening issue reporter'); + this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); - this._issueWindow.on('close', () => this._issueWindow = null); + this._issueWindow.on('close', () => this._issueWindow = null); - this._issueParentWindow.on('closed', () => { - if (this._issueWindow) { - this._issueWindow.close(); - this._issueWindow = null; - } - }); - } + this._issueParentWindow.on('closed', () => { + if (this._issueWindow) { + this._issueWindow.close(); + this._issueWindow = null; + } + }); } + } - if (this._issueWindow) { - this._issueWindow.focus(); - } - }); + if (this._issueWindow) { + this._issueWindow.focus(); + } } - openProcessExplorer(data: ProcessExplorerData): Promise { - return new Promise(_ => { - // Create as singleton - if (!this._processExplorerWindow) { - this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); - if (this._processExplorerParentWindow) { - const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); - this._processExplorerWindow = new BrowserWindow({ - skipTaskbar: true, - resizable: true, - fullscreen: false, - width: position.width, - height: position.height, - minWidth: 300, - minHeight: 200, - x: position.x, - y: position.y, - backgroundColor: data.styles.backgroundColor, - title: localize('processExplorer', "Process Explorer"), - webPreferences: { - preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - enableWebSQL: false, - enableRemoteModule: false, - spellcheck: false, - nativeWindowOpen: true, - zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), - sandbox: true, - contextIsolation: true - } - }); + async openProcessExplorer(data: ProcessExplorerData): Promise { + // Create as singleton + if (!this._processExplorerWindow) { + this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); + if (this._processExplorerParentWindow) { + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); + this._processExplorerWindow = new BrowserWindow({ + skipTaskbar: true, + resizable: true, + fullscreen: false, + width: position.width, + height: position.height, + minWidth: 300, + minHeight: 200, + x: position.x, + y: position.y, + backgroundColor: data.styles.backgroundColor, + title: localize('processExplorer', "Process Explorer"), + webPreferences: { + preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, + enableWebSQL: false, + enableRemoteModule: false, + spellcheck: false, + nativeWindowOpen: true, + zoomFactor: zoomLevelToZoomFactor(data.zoomLevel), + sandbox: true, + contextIsolation: true + } + }); - this._processExplorerWindow.setMenuBarVisibility(false); + this._processExplorerWindow.setMenuBarVisibility(false); - const windowConfiguration = { - appRoot: this.environmentService.appRoot, - windowId: this._processExplorerWindow.id, - userEnv: this.userEnv, - machineId: this.machineId, - data - }; + const windowConfiguration = { + appRoot: this.environmentService.appRoot, + windowId: this._processExplorerWindow.id, + userEnv: this.userEnv, + machineId: this.machineId, + data + }; - this._processExplorerWindow.loadURL( - toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); + this._processExplorerWindow.loadURL( + toWindowUrl('vs/code/electron-sandbox/processExplorer/processExplorer.html', windowConfiguration)); - this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); + this._processExplorerWindow.on('close', () => this._processExplorerWindow = null); - this._processExplorerParentWindow.on('close', () => { - if (this._processExplorerWindow) { - this._processExplorerWindow.close(); - this._processExplorerWindow = null; - } - }); - } + this._processExplorerParentWindow.on('close', () => { + if (this._processExplorerWindow) { + this._processExplorerWindow.close(); + this._processExplorerWindow = null; + } + }); } + } - // Focus - if (this._processExplorerWindow) { - this._processExplorerWindow.focus(); - } - }); + // Focus + if (this._processExplorerWindow) { + this._processExplorerWindow.focus(); + } } public async getSystemStatus(): Promise { diff --git a/src/vs/platform/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 9b3d6566394..61e66f0fd6a 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -9,7 +9,7 @@ import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; @@ -113,7 +113,7 @@ export class LaunchMainService implements ILaunchMainService { } } - private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { + private async startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise { const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP; let usedWindows: ICodeWindow[] = []; @@ -205,17 +205,15 @@ export class LaunchMainService implements ILaunchMainService { whenDeleted(waitMarkerFileURI.fsPath) ]).then(() => undefined, () => undefined); } - - return Promise.resolve(undefined); } - getMainProcessId(): Promise { + async getMainProcessId(): Promise { this.logService.trace('Received request for process ID from other instance.'); - return Promise.resolve(process.pid); + return process.pid; } - getMainProcessInfo(): Promise { + async getMainProcessInfo(): Promise { this.logService.trace('Received request for main process info from other instance.'); const windows: IWindowInfo[] = []; @@ -228,18 +226,18 @@ export class LaunchMainService implements ILaunchMainService { } }); - return Promise.resolve({ + return { mainPID: process.pid, mainArguments: process.argv.slice(1), windows, screenReader: !!app.accessibilitySupportEnabled, gpuFeatureStatus: app.getGPUFeatureStatus() - }); + }; } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { + async getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); - const promises: Promise[] = windows.map(window => { + const diagnostics: Array = await Promise.all(windows.map(window => { return new Promise((resolve) => { const remoteAuthority = window.remoteAuthority; if (remoteAuthority) { @@ -267,9 +265,9 @@ export class LaunchMainService implements ILaunchMainService { resolve(undefined); } }); - }); + })); - return Promise.all(promises).then(diagnostics => diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x)); + return diagnostics.filter((x): x is IRemoteDiagnosticInfo | IRemoteDiagnosticError => !!x); } private getFolderURIs(window: ICodeWindow): URI[] { @@ -296,6 +294,7 @@ export class LaunchMainService implements ILaunchMainService { private codeWindowToInfo(window: ICodeWindow): IWindowInfo { const folderURIs = this.getFolderURIs(window); + return this.browserWindowToInfo(window.win, folderURIs, window.remoteAuthority); } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index f3f321125c3..b553a05811c 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -126,7 +126,7 @@ const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardN const treeIndentKey = 'workbench.tree.indent'; const treeRenderIndentGuidesKey = 'workbench.tree.renderIndentGuides'; const listSmoothScrolling = 'workbench.list.smoothScrolling'; -const treeExpandOnFolderClick = 'workbench.tree.expandOnFolderClick'; +const treeExpandMode = 'workbench.tree.expandMode'; function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; @@ -445,11 +445,11 @@ abstract class ResourceNavigator extends Disposable { private readonly openOnFocus: boolean; private openOnSingleClick: boolean; - private readonly _onDidOpen = this._register(new Emitter>()); - readonly onDidOpen: Event> = this._onDidOpen.event; + private readonly _onDidOpen = this._register(new Emitter>()); + readonly onDidOpen: Event> = this._onDidOpen.event; constructor( - private readonly widget: ListWidget, + protected readonly widget: ListWidget, options?: IResourceNavigatorOptions ) { super(); @@ -478,12 +478,11 @@ abstract class ResourceNavigator extends Disposable { const focus = this.widget.getFocus(); this.widget.setSelection(focus, event.browserEvent); - const element = this.widget.getSelection()[0]; const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; const pinned = !preserveFocus; const sideBySide = false; - this._open(element, preserveFocus, pinned, sideBySide, event.browserEvent); + this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); } private onSelectionFromKeyboard(event: ITreeEvent): void { @@ -491,12 +490,11 @@ abstract class ResourceNavigator extends Disposable { return; } - const element = this.widget.getSelection()[0]; const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true; const pinned = !preserveFocus; const sideBySide = false; - this._open(element, preserveFocus, pinned, sideBySide, event.browserEvent); + this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent); } private onPointer(element: T | undefined, browserEvent: MouseEvent): void { @@ -546,23 +544,35 @@ abstract class ResourceNavigator extends Disposable { browserEvent }); } + + abstract getSelectedElement(): T | undefined; } -export class ListResourceNavigator extends ResourceNavigator { +export class ListResourceNavigator extends ResourceNavigator { + constructor( - list: List | PagedList, + protected readonly widget: List | PagedList, options?: IResourceNavigatorOptions ) { - super(list, options); + super(widget, options); + } + + getSelectedElement(): T | undefined { + return this.widget.getSelectedElements()[0]; } } class TreeResourceNavigator extends ResourceNavigator { + constructor( - tree: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, + protected readonly widget: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, options: IResourceNavigatorOptions ) { - super(tree, options); + super(widget, options); + } + + getSelectedElement(): T | undefined { + return this.widget.getSelection()[0] ?? undefined; } } @@ -597,7 +607,7 @@ export class WorkbenchObjectTree, TFilterData = void> private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -633,7 +643,7 @@ export class WorkbenchCompressibleObjectTree, TFilter private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -677,7 +687,7 @@ export class WorkbenchDataTree extends DataTree; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -722,7 +732,7 @@ export class WorkbenchAsyncDataTree extends Async private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -764,7 +774,7 @@ export class WorkbenchCompressibleAsyncDataTree e private internals: WorkbenchTreeInternals; get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; } get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; } - get onDidOpen(): Event> { return this.internals.onDidOpen; } + get onDidOpen(): Event> { return this.internals.onDidOpen; } constructor( user: string, @@ -839,7 +849,7 @@ function workbenchTreeDataPreamble(treeExpandOnFolderClick) + expandOnlyOnTwistieClick: options.expandOnlyOnTwistieClick ?? (configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick') } as TOptions }; } @@ -855,7 +865,7 @@ class WorkbenchTreeInternals { private styler: IDisposable | undefined; private navigator: TreeResourceNavigator; - get onDidOpen(): Event> { return this.navigator.onDidOpen; } + get onDidOpen(): Event> { return this.navigator.onDidOpen; } constructor( private tree: WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, @@ -941,8 +951,8 @@ class WorkbenchTreeInternals { if (e.affectsConfiguration(openModeSettingKey)) { newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' }; } - if (e.affectsConfiguration(treeExpandOnFolderClick) && options.expandOnlyOnTwistieClick === undefined) { - newOptions = { ...newOptions, expandOnlyOnTwistieClick: !configurationService.getValue(treeExpandOnFolderClick) }; + if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) { + newOptions = { ...newOptions, expandOnlyOnTwistieClick: configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick' }; } if (Object.keys(newOptions).length > 0) { tree.updateOptions(newOptions); @@ -1048,10 +1058,11 @@ configurationRegistry.registerConfiguration({ 'default': true, markdownDescription: localize('automatic keyboard navigation setting', "Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.") }, - [treeExpandOnFolderClick]: { - type: 'boolean', - default: true, - description: localize('list expand on folder click setting', "Controls whether tree folders are expanded when clicking the folder names."), + [treeExpandMode]: { + type: 'string', + enum: ['singleClick', 'doubleClick'], + default: 'singleClick', + description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names."), } } }); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 3039b01a1cf..215be10e210 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -6,9 +6,9 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; +import { app, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, KeyboardEvent } from 'electron'; import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; @@ -62,7 +62,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: Event) => void } = Object.create(null); + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow | undefined, event: KeyboardEvent) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -639,7 +639,7 @@ export class Menubar { private createMenuItem(label: string, click: () => void, enabled?: boolean, checked?: boolean): MenuItem; private createMenuItem(arg1: string, arg2: any, arg3?: boolean, arg4?: boolean): MenuItem { const label = this.mnemonicLabel(arg1); - const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: Event) => { + const click: () => void = (typeof arg2 === 'function') ? arg2 : (menuItem: MenuItem & IMenuItemWithKeybinding, win: BrowserWindow, event: KeyboardEvent) => { const userSettingsLabel = menuItem ? menuItem.userSettingsLabel : null; let commandId = arg2; if (Array.isArray(arg2)) { diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index e24efa83cf1..57b2a5c8822 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -6,7 +6,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor, nativeTheme } from 'electron'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; @@ -15,7 +15,7 @@ import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platf import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { dirExists } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index 8645255d9e7..67195995c28 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -200,12 +200,10 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS return this.channel.call('updateItems', serializableRequest); } - close(): Promise { + async close(): Promise { // when we are about to close, we start to ignore main-side changes since we close anyway dispose(this.onDidChangeItemsOnMainListener); - - return Promise.resolve(); // global storage is closed on the main side } dispose(): void { diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index 70f67e0f33e..b5239353a96 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -358,7 +358,7 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: */ export const listFocusBackground = registerColor('list.focusBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); -export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0074E8', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); +export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.")); diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 68b66ae159b..a19a41a84dd 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -111,7 +111,7 @@ class IconRegistry implements IIconRegistry { if (existing) { if (description && !existing.description) { existing.description = description; - this.iconSchema.properties[id].markdownDescription = `${description}: $(${id})`; + this.iconSchema.properties[id].markdownDescription = `${description} $(${id})`; const enumIndex = this.iconReferenceSchema.enum.indexOf(id); if (enumIndex !== -1) { this.iconReferenceSchema.enumDescriptions[enumIndex] = description; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 8246ccbf1be..2fb802fbce9 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -811,7 +811,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitPastWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this.undo(strResource)); + return new WorkspaceVerificationError(this._undo(strResource)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -959,7 +959,7 @@ export class UndoRedoService implements IUndoRedoService { if (result.choice === 1) { // choice: undo this file this._splitPastWorkspaceElement(element, null); - return this.undo(strResource); + return this._undo(strResource); } // choice: undo in all files @@ -1044,16 +1044,22 @@ export class UndoRedoService implements IUndoRedoService { const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId); if (matchedStrResource) { - return this.undo(matchedStrResource); + return this._undo(matchedStrResource); } } - public undo(resourceOrSource: URI | UndoRedoSource | string): Promise | void { + public undo(resourceOrSource: URI | UndoRedoSource): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this.undo(matchedStrResource) : undefined; + return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id) : undefined; } - const strResource = typeof resourceOrSource === 'string' ? resourceOrSource : this.getUriComparisonKey(resourceOrSource); + if (typeof resourceOrSource === 'string') { + return this._undo(resourceOrSource); + } + return this._undo(this.getUriComparisonKey(resourceOrSource)); + } + + private _undo(strResource: string, sourceId: number = 0): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1069,10 +1075,15 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be undone before this one - return this.undo(matchedStrResource); + return this._undo(matchedStrResource); } } + if (element.sourceId !== sourceId) { + // Hit a different source, prompt for confirmation + return this._confirmDifferentSourceAndContinueUndo(strResource, element); + } + try { if (element.type === UndoRedoElementType.Workspace) { return this._workspaceUndo(strResource, element); @@ -1086,6 +1097,28 @@ export class UndoRedoService implements IUndoRedoService { } } + private async _confirmDifferentSourceAndContinueUndo(strResource: string, element: StackElement): Promise { + const result = await this._dialogService.show( + Severity.Info, + nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label), + [ + nls.localize('confirmDifferentSource.ok', "Undo"), + nls.localize('cancel', "Cancel"), + ], + { + cancelId: 1 + } + ); + + if (result.choice === 1) { + // choice: cancel + return; + } + + // choice: undo + return this._undo(strResource, element.sourceId); + } + private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] { if (!sourceId) { return [null, null]; @@ -1128,7 +1161,7 @@ export class UndoRedoService implements IUndoRedoService { if (element.canSplit()) { this._splitFutureWorkspaceElement(element, ignoreResources); this._notificationService.info(message); - return new WorkspaceVerificationError(this.redo(strResource)); + return new WorkspaceVerificationError(this._redo(strResource)); } else { // Cannot safely split this workspace element => flush all undo/redo stacks for (const strResource of element.strResources) { @@ -1300,16 +1333,22 @@ export class UndoRedoService implements IUndoRedoService { const [, matchedStrResource] = this._findClosestRedoElementInGroup(groupId); if (matchedStrResource) { - return this.redo(matchedStrResource); + return this._redo(matchedStrResource); } } public redo(resourceOrSource: URI | UndoRedoSource | string): Promise | void { if (resourceOrSource instanceof UndoRedoSource) { const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id); - return matchedStrResource ? this.redo(matchedStrResource) : undefined; + return matchedStrResource ? this._redo(matchedStrResource) : undefined; } - const strResource = typeof resourceOrSource === 'string' ? resourceOrSource : this.getUriComparisonKey(resourceOrSource); + if (typeof resourceOrSource === 'string') { + return this._redo(resourceOrSource); + } + return this._redo(this.getUriComparisonKey(resourceOrSource)); + } + + private _redo(strResource: string): Promise | void { if (!this._editStacks.has(strResource)) { return; } @@ -1325,7 +1364,7 @@ export class UndoRedoService implements IUndoRedoService { const [matchedElement, matchedStrResource] = this._findClosestRedoElementInGroup(element.groupId); if (element !== matchedElement && matchedStrResource) { // there is an element in the same group that should be redone before this one - return this.redo(matchedStrResource); + return this._redo(matchedStrResource); } } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index e716e1355f4..2158147262b 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -528,10 +528,32 @@ export class ExtensionsInitializer extends AbstractInitializer { } else { toInstall.names.push(extension.identifier.id); } + if (extension.disabled) { + toDisable.push(extension.identifier); + } } } } + // 1. Initialise already installed extensions state + for (const extensionToSync of installedExtensionsToSync) { + if (extensionToSync.state) { + const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}'); + forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value); + this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); + } + } + + // 2. Initialise extensions enablement + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Disabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Disabling extension`, identifier.id); + } + } + + // 3. Install extensions if (toInstall.names.length || toInstall.uuids.length) { const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; for (const galleryExtension of galleryExtensions) { @@ -552,22 +574,6 @@ export class ExtensionsInitializer extends AbstractInitializer { } } - if (toDisable.length) { - for (const identifier of toDisable) { - this.logService.trace(`Enabling extension...`, identifier.id); - await this.extensionEnablementService.disableExtension(identifier); - this.logService.info(`Enabled extension`, identifier.id); - } - } - - for (const extensionToSync of installedExtensionsToSync) { - if (extensionToSync.state) { - const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}'); - forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value); - this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE); - } - } - return newlyEnabledExtensions; } diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts index a019d267e69..ce6233924b0 100644 --- a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -125,7 +125,10 @@ export class WebviewProtocolProvider extends Disposable { } } - private async handleWebviewRequest(request: Electron.Request, callback: any) { + private async handleWebviewRequest( + request: Electron.ProtocolRequest, + callback: (response: string | Electron.ProtocolResponse) => void + ) { try { const uri = URI.parse(request.url); const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path); @@ -144,8 +147,8 @@ export class WebviewProtocolProvider extends Disposable { } private async handleWebviewResourceRequest( - request: Electron.Request, - callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void + request: Electron.ProtocolRequest, + callback: (stream: NodeJS.ReadableStream | Electron.ProtocolResponse) => void ) { try { const uri = URI.parse(request.url); @@ -220,14 +223,14 @@ export class WebviewProtocolProvider extends Disposable { if (result.type === WebviewResourceResponse.Type.AccessDenied) { console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ data: null, statusCode: 401 }); + return callback({ data: undefined, statusCode: 401 }); } } } catch { // noop } - return callback({ data: null, statusCode: 404 }); + return callback({ data: undefined, statusCode: 404 }); } public didLoadResource(requestId: number, content: VSBuffer | undefined) { diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 40c952136ec..ca3e63d96ed 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -115,7 +115,6 @@ export interface IWindowSettings { enableMenuBarMnemonics: boolean; closeWhenEmpty: boolean; clickThroughInactive: boolean; - enableExperimentalProxyLoginDialog: boolean; } export function getTitleBarStyle(configurationService: IConfigurationService): 'native' | 'custom' { diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/electron-main/window.ts similarity index 100% rename from src/vs/platform/windows/node/window.ts rename to src/vs/platform/windows/electron-main/window.ts diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 83eccca73c4..e77f247e08a 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowOpenable, IOpenEmptyWindowOptions, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { OpenContext } from 'vs/platform/windows/node/window'; +import { OpenContext } from 'vs/platform/windows/electron-main/window'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 74beab39da6..0fe8e924f00 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; +import { statSync, unlink } from 'fs'; import { basename, normalize, join, posix } from 'vs/base/common/path'; import { localize } from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { coalesce, distinct, firstOrDefault } from 'vs/base/common/arrays'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -18,7 +18,7 @@ import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMai import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor, INativeWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/node/window'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, OpenContext } from 'vs/platform/windows/electron-main/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; @@ -34,7 +34,7 @@ import { restoreWindowsState, WindowsStateStorageData, getWindowsStateStoreData import { getWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { withNullAsUndefined } from 'vs/base/common/types'; import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; @@ -447,46 +447,55 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Open based on config - const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd); + const { windows: usedWindows, filesOpenedInWindow } = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, emptyToRestore, emptyToOpen, filesToOpen, foldersToAdd); this.logService.trace(`windowsManager#open used window count ${usedWindows.length} (workspacesToOpen: ${workspacesToOpen.length}, foldersToOpen: ${foldersToOpen.length}, emptyToRestore: ${emptyToRestore.length}, emptyToOpen: ${emptyToOpen})`); // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); - let focusLastOpened = true; - let focusLastWindow = true; - // 1.) focus last active window if we are not instructed to open any paths - if (focusLastActive) { - const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); - if (lastActiveWindow.length) { - lastActiveWindow[0].focus(); - focusLastOpened = false; - focusLastWindow = false; - } + // 1.) focus window we opened files in always with highest priority + if (filesOpenedInWindow) { + filesOpenedInWindow.focus(); } - // 2.) if instructed to open paths, focus last window which is not restored - if (focusLastOpened) { - for (let i = usedWindows.length - 1; i >= 0; i--) { - const usedWindow = usedWindows[i]; - if ( - (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace - (usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window - ) { - continue; + // Otherwise, find a good window based on open params + else { + const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && !openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); + let focusLastOpened = true; + let focusLastWindow = true; + + // 2.) focus last active window if we are not instructed to open any paths + if (focusLastActive) { + const lastActiveWindow = usedWindows.filter(window => this.windowsState.lastActiveWindow && window.backupPath === this.windowsState.lastActiveWindow.backupPath); + if (lastActiveWindow.length) { + lastActiveWindow[0].focus(); + focusLastOpened = false; + focusLastWindow = false; } - - usedWindow.focus(); - focusLastWindow = false; - break; } - } - // 3.) finally, always ensure to have at least last used window focused - if (focusLastWindow) { - usedWindows[usedWindows.length - 1].focus(); + // 3.) if instructed to open paths, focus last window which is not restored + if (focusLastOpened) { + for (let i = usedWindows.length - 1; i >= 0; i--) { + const usedWindow = usedWindows[i]; + if ( + (usedWindow.openedWorkspace && workspacesToRestore.some(workspace => usedWindow.openedWorkspace && workspace.workspace.id === usedWindow.openedWorkspace.id)) || // skip over restored workspace + (usedWindow.backupPath && emptyToRestore.some(empty => usedWindow.backupPath && empty.backupFolder === basename(usedWindow.backupPath))) // skip over restored empty window + ) { + continue; + } + + usedWindow.focus(); + focusLastWindow = false; + break; + } + } + + // 4.) finally, always ensure to have at least last used window focused + if (focusLastWindow) { + usedWindows[usedWindows.length - 1].focus(); + } } } @@ -513,7 +522,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => fs.unlink(waitMarkerFileURI.fsPath, _error => undefined)); + usedWindows[0].whenClosedOrLoaded.then(() => unlink(waitMarkerFileURI.fsPath, () => undefined)); } return usedWindows; @@ -537,8 +546,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic emptyToOpen: number, filesToOpen: IFilesToOpen | undefined, foldersToAdd: IFolderPathToOpen[] - ) { + ): { windows: ICodeWindow[], filesOpenedInWindow: ICodeWindow | undefined } { const usedWindows: ICodeWindow[] = []; + let filesOpenedInWindow: ICodeWindow | undefined = undefined; // Settings can decide if files/folders open in new window or not let { openFolderInNewWindow, openFilesInNewWindow } = this.shouldOpenNewWindow(openConfig); @@ -587,16 +597,18 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic else { // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen)); + const window = this.doOpenFilesInExistingWindow(openConfig, bestWindowOrFolder, filesToOpen); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used filesToOpen = undefined; + filesOpenedInWindow = window; } } // Finally, if no window or folder is found, just open the files in an empty window else { - usedWindows.push(this.openInBrowserWindow({ + const window = this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -604,29 +616,33 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, remoteAuthority: filesToOpen.remoteAuthority, forceNewTabbedWindow: openConfig.forceNewTabbedWindow - })); + }); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used filesToOpen = undefined; + filesOpenedInWindow = window; } } // Handle workspaces to open (instructed and to restore) - const allWorkspacesToOpen = arrays.distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates + const allWorkspacesToOpen = distinct(workspacesToOpen, workspace => workspace.workspace.id); // prevent duplicates if (allWorkspacesToOpen.length > 0) { // Check for existing instances - const windowsOnWorkspace = arrays.coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); + const windowsOnWorkspace = coalesce(allWorkspacesToOpen.map(workspaceToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, workspaceToOpen.workspace))); if (windowsOnWorkspace.length > 0) { const windowOnWorkspace = windowsOnWorkspace[0]; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === windowOnWorkspace.remoteAuthority) ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow)); + const window = this.doOpenFilesInExistingWindow(openConfig, windowOnWorkspace, filesToOpenInWindow); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used if (filesToOpenInWindow) { filesToOpen = undefined; + filesOpenedInWindow = window; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -642,11 +658,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow)); + const window = this.doOpenFolderOrWorkspace(openConfig, workspaceToOpen, openFolderInNewWindow, filesToOpenInWindow); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used if (filesToOpenInWindow) { filesToOpen = undefined; + filesOpenedInWindow = window; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -654,21 +672,23 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Handle folders to open (instructed and to restore) - const allFoldersToOpen = arrays.distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.folderUri)); // prevent duplicates + const allFoldersToOpen = distinct(foldersToOpen, folder => extUriBiasedIgnorePathCase.getComparisonKey(folder.folderUri)); // prevent duplicates if (allFoldersToOpen.length > 0) { // Check for existing instances - const windowsOnFolderPath = arrays.coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); + const windowsOnFolderPath = coalesce(allFoldersToOpen.map(folderToOpen => findWindowOnWorkspace(WindowsMainService.WINDOWS, folderToOpen.folderUri))); if (windowsOnFolderPath.length > 0) { const windowOnFolderPath = windowsOnFolderPath[0]; const filesToOpenInWindow = filesToOpen?.remoteAuthority === windowOnFolderPath.remoteAuthority ? filesToOpen : undefined; // Do open files - usedWindows.push(this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow)); + const window = this.doOpenFilesInExistingWindow(openConfig, windowOnFolderPath, filesToOpenInWindow); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used if (filesToOpenInWindow) { filesToOpen = undefined; + filesOpenedInWindow = window; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -685,11 +705,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; // Do open folder - usedWindows.push(this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow)); + const window = this.doOpenFolderOrWorkspace(openConfig, folderToOpen, openFolderInNewWindow, filesToOpenInWindow); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used if (filesToOpenInWindow) { filesToOpen = undefined; + filesOpenedInWindow = window; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -697,13 +719,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic } // Handle empty to restore - const allEmptyToRestore = arrays.distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates + const allEmptyToRestore = distinct(emptyToRestore, info => info.backupFolder); // prevent duplicates if (allEmptyToRestore.length > 0) { allEmptyToRestore.forEach(emptyWindowBackupInfo => { const remoteAuthority = emptyWindowBackupInfo.remoteAuthority; const filesToOpenInWindow = (filesToOpen?.remoteAuthority === remoteAuthority) ? filesToOpen : undefined; - usedWindows.push(this.openInBrowserWindow({ + const window = this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -712,11 +734,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewWindow: true, forceNewTabbedWindow: openConfig.forceNewTabbedWindow, emptyWindowBackupInfo - })); + }); + usedWindows.push(window); - // Reset these because we handled them + // Reset `filesToOpen` because we handled them and also remember window we used if (filesToOpenInWindow) { filesToOpen = undefined; + filesOpenedInWindow = window; } openFolderInNewWindow = true; // any other folders to open must open in new window then @@ -732,15 +756,21 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = filesToOpen ? filesToOpen.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen)); + const window = this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, filesToOpen); + usedWindows.push(window); - // Reset these because we handled them - filesToOpen = undefined; - openFolderInNewWindow = true; // any other window to open must open in new window then + // Reset `filesToOpen` because we handled them and also remember window we used + if (filesToOpen) { + filesToOpen = undefined; + filesOpenedInWindow = window; + } + + // any other window to open must open in new window then + openFolderInNewWindow = true; } } - return arrays.distinct(usedWindows); + return { windows: distinct(usedWindows), filesOpenedInWindow }; } private doOpenFilesInExistingWindow(configuration: IOpenConfiguration, window: ICodeWindow, filesToOpen?: IFilesToOpen): ICodeWindow { @@ -1163,7 +1193,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let candidate = normalize(anyPath); try { - const candidateStat = fs.statSync(candidate); + const candidateStat = statSync(candidate); if (candidateStat.isFile()) { // Workspace (unless disabled via flag) @@ -1697,7 +1727,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic getWindowById(windowId: number): ICodeWindow | undefined { const res = WindowsMainService.WINDOWS.filter(window => window.id === windowId); - return arrays.firstOrDefault(res); + return firstOrDefault(res); } getWindowByWebContents(webContents: WebContents): ICodeWindow | undefined { diff --git a/src/vs/platform/windows/test/node/window.test.ts b/src/vs/platform/windows/test/electron-main/window.test.ts similarity index 96% rename from src/vs/platform/windows/test/node/window.test.ts rename to src/vs/platform/windows/test/electron-main/window.test.ts index 576d30d134a..1beec3358a4 100644 --- a/src/vs/platform/windows/test/node/window.test.ts +++ b/src/vs/platform/windows/test/electron-main/window.test.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as path from 'vs/base/common/path'; -import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/node/window'; +import { IBestWindowOrFolderOptions, IWindowContext, findBestWindowOrFolderForFile, OpenContext } from 'vs/platform/windows/electron-main/window'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; const fixturesFolder = getPathFromAmdModule(require, './fixtures'); @@ -17,7 +18,7 @@ const testWorkspace: IWorkspaceIdentifier = { configPath: URI.file(path.join(fixturesFolder, 'workspaces.json')) }; -const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath); +const testWorkspaceFolders = toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }], testWorkspace.configPath, extUriBiasedIgnorePathCase); function options(custom?: Partial>): IBestWindowOrFolderOptions { return { diff --git a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts b/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts index 419caee0d71..e776f85c227 100644 --- a/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts +++ b/src/vs/platform/windows/test/electron-main/windowsStateStorage.test.ts @@ -286,7 +286,5 @@ suite('Windows State Storing', () => { } }; assertEqualWindowsState(expected, windowsState, 'v1_32_empty_window'); - }); - }); diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 72ca615f926..13ec5a768e9 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -234,16 +234,16 @@ export function toWorkspaceFolder(resource: URI): WorkspaceFolder { return new WorkspaceFolder({ uri: resource, index: 0, name: resources.basenameOrAuthority(resource) }, { uri: resource.toString() }); } -export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI): WorkspaceFolder[] { +export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], workspaceConfigFile: URI, extUri: resources.IExtUri): WorkspaceFolder[] { let result: WorkspaceFolder[] = []; let seen: Set = new Set(); - const relativeTo = resources.dirname(workspaceConfigFile); + const relativeTo = extUri.dirname(workspaceConfigFile); for (let configuredFolder of configuredFolders) { let uri: URI | null = null; if (isRawFileWorkspaceFolder(configuredFolder)) { if (configuredFolder.path) { - uri = resources.resolvePath(relativeTo, configuredFolder.path); + uri = extUri.resolvePath(relativeTo, configuredFolder.path); } } else if (isRawUriWorkspaceFolder(configuredFolder)) { try { @@ -259,11 +259,11 @@ export function toWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[], } if (uri) { // remove duplicates - let comparisonKey = resources.getComparisonKey(uri); + let comparisonKey = extUri.getComparisonKey(uri); if (!seen.has(comparisonKey)) { seen.add(comparisonKey); - const name = configuredFolder.name || resources.basenameOrAuthority(uri); + const name = configuredFolder.name || extUri.basenameOrAuthority(uri); result.push(new WorkspaceFolder({ uri, name, index: result.length }, configuredFolder)); } } diff --git a/src/vs/platform/workspace/test/common/workspace.test.ts b/src/vs/platform/workspace/test/common/workspace.test.ts index 1d3f78f7b1a..00864798f86 100644 --- a/src/vs/platform/workspace/test/common/workspace.test.ts +++ b/src/vs/platform/workspace/test/common/workspace.test.ts @@ -9,6 +9,7 @@ import { Workspace, toWorkspaceFolders, WorkspaceFolder } from 'vs/platform/work import { URI } from 'vs/base/common/uri'; import { IRawFileWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { isLinux, isWindows } from 'vs/base/common/platform'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; suite('Workspace', () => { @@ -70,7 +71,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder', () => { - const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -80,7 +81,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single relative folder', () => { - const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: './test' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); assert.equal(actual[0].uri.fsPath, testFolderUri.fsPath); @@ -90,7 +91,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with single absolute folder with name', () => { - const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test', name: 'hello' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 1); @@ -101,7 +102,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -121,7 +122,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute folders with names', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -141,7 +142,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple unique absolute and relative folders', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/abc/test3', name: 'noName' }, { path: './test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -161,7 +162,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test2', name: 'noName' }, { path: '/src/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 2); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -176,7 +177,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with duplicates', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '/src/test3', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); @@ -196,7 +197,7 @@ suite('Workspace', () => { }); test('toWorkspaceFolders with multiple absolute and relative folders with invalid paths', () => { - const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri); + const actual = toWorkspaceFolders([{ path: '/src/test2' }, { path: '', name: 'noName' }, { path: './test3' }, { path: '/abc/test1' }], workspaceConfigUri, extUriBiasedIgnorePathCase); assert.equal(actual.length, 3); assert.equal(actual[0].uri.fsPath, test2FolderUri.fsPath); diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index d6643801ab6..0aef9c76cf3 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -9,7 +9,7 @@ import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/works import { URI, UriComponents } from 'vs/base/common/uri'; import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { extname, isAbsolute } from 'vs/base/common/path'; -import { dirname, resolvePath, isEqualAuthority, relativePath, extname as resourceExtname, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { extname as resourceExtname, extUriBiasedIgnorePathCase, IExtUri } from 'vs/base/common/resources'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import * as json from 'vs/base/common/json'; import { Schemas } from 'vs/base/common/network'; @@ -212,12 +212,12 @@ const SLASH = '/'; * @param targetConfigFolderURI the folder where the workspace is living in * @param useSlashForPath if set, use forward slashes for file paths on windows */ -export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows): IStoredWorkspaceFolder { +export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, folderName: string | undefined, targetConfigFolderURI: URI, useSlashForPath = !isWindows, extUri: IExtUri): IStoredWorkspaceFolder { if (folderURI.scheme !== targetConfigFolderURI.scheme) { return { name: folderName, uri: folderURI.toString(true) }; } - let folderPath = !forceAbsolute ? relativePath(targetConfigFolderURI, folderURI) : undefined; + let folderPath = !forceAbsolute ? extUri.relativePath(targetConfigFolderURI, folderURI) : undefined; if (folderPath !== undefined) { if (folderPath.length === 0) { folderPath = '.'; @@ -241,7 +241,7 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, } } } else { - if (!isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { + if (!extUri.isEqualAuthority(folderURI.authority, targetConfigFolderURI.authority)) { return { name: folderName, uri: folderURI.toString(true) }; } folderPath = folderURI.path; @@ -255,17 +255,17 @@ export function getStoredWorkspaceFolder(folderURI: URI, forceAbsolute: boolean, * Rewrites the content of a workspace file to be saved at a new location. * Throws an exception if file is not a valid workspace file */ -export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI) { +export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, configPathURI: URI, isFromUntitledWorkspace: boolean, targetConfigPathURI: URI, extUri: IExtUri) { let storedWorkspace = doParseStoredWorkspace(configPathURI, rawWorkspaceContents); - const sourceConfigFolder = dirname(configPathURI); - const targetConfigFolder = dirname(targetConfigPathURI); + const sourceConfigFolder = extUri.dirname(configPathURI); + const targetConfigFolder = extUri.dirname(targetConfigPathURI); const rewrittenFolders: IStoredWorkspaceFolder[] = []; const slashForPath = useSlashForPath(storedWorkspace.folders); for (const folder of storedWorkspace.folders) { - const folderURI = isRawFileWorkspaceFolder(folder) ? resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); + const folderURI = isRawFileWorkspaceFolder(folder) ? extUri.resolvePath(sourceConfigFolder, folder.path) : URI.parse(folder.uri); let absolute; if (isFromUntitledWorkspace) { // if it was an untitled workspace, try to make paths relative @@ -274,7 +274,7 @@ export function rewriteWorkspaceFileForNewLocation(rawWorkspaceContents: string, // for existing workspaces, preserve whether a path was absolute or relative absolute = !isRawFileWorkspaceFolder(folder) || isAbsolute(folder.path); } - rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath)); + rewrittenFolders.push(getStoredWorkspaceFolder(folderURI, absolute, folder.name, targetConfigFolder, slashForPath, extUri)); } // Preserve as much of the existing workspace as possible by using jsonEdit diff --git a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts index 7a5baa225e5..f174547116c 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import * as arrays from 'vs/base/common/arrays'; +import { localize } from 'vs/nls'; +import { coalesce } from 'vs/base/common/arrays'; import { IStateService } from 'vs/platform/state/node/state'; import { app, JumpListCategory } from 'electron'; import { ILogService } from 'vs/platform/log/common/log'; @@ -14,7 +14,7 @@ import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData, WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { isEqual, dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; +import { dirname, originalFSPath, basename, extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; @@ -57,10 +57,10 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa declare readonly _serviceBrand: undefined; - private readonly _onRecentlyOpenedChange = new Emitter(); + private readonly _onRecentlyOpenedChange = this._register(new Emitter()); readonly onRecentlyOpenedChange: CommonEvent = this._onRecentlyOpenedChange.event; - private macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); + private readonly macOSRecentDocumentsUpdater = this._register(new ThrottledDelayer(800)); constructor( @IStateService private readonly stateService: IStateService, @@ -89,40 +89,40 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } this.updateWindowsJumpList(); - this.onRecentlyOpenedChange(() => this.updateWindowsJumpList()); + this._register(this.onRecentlyOpenedChange(() => this.updateWindowsJumpList())); } - addRecentlyOpened(newlyAdded: IRecent[]): void { + addRecentlyOpened(recentToAdd: IRecent[]): void { const workspaces: Array = []; const files: IRecentFile[] = []; - for (let curr of newlyAdded) { + for (let recent of recentToAdd) { // Workspace - if (isRecentWorkspace(curr)) { - if (!this.workspacesMainService.isUntitledWorkspace(curr.workspace) && indexOfWorkspace(workspaces, curr.workspace) === -1) { - workspaces.push(curr); + if (isRecentWorkspace(recent)) { + if (!this.workspacesMainService.isUntitledWorkspace(recent.workspace) && indexOfWorkspace(workspaces, recent.workspace) === -1) { + workspaces.push(recent); } } // Folder - else if (isRecentFolder(curr)) { - if (indexOfFolder(workspaces, curr.folderUri) === -1) { - workspaces.push(curr); + else if (isRecentFolder(recent)) { + if (indexOfFolder(workspaces, recent.folderUri) === -1) { + workspaces.push(recent); } } // File else { - const alreadyExistsInHistory = indexOfFile(files, curr.fileUri) >= 0; - const shouldBeFiltered = curr.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(curr.fileUri)) >= 0; + const alreadyExistsInHistory = indexOfFile(files, recent.fileUri) >= 0; + const shouldBeFiltered = recent.fileUri.scheme === Schemas.file && WorkspacesHistoryMainService.COMMON_FILES_FILTER.indexOf(basename(recent.fileUri)) >= 0; if (!alreadyExistsInHistory && !shouldBeFiltered) { - files.push(curr); + files.push(recent); // Add to recent documents (Windows only, macOS later) - if (isWindows && curr.fileUri.scheme === Schemas.file) { - app.addRecentDocument(curr.fileUri.fsPath); + if (isWindows && recent.fileUri.scheme === Schemas.file) { + app.addRecentDocument(recent.fileUri.fsPath); } } } @@ -147,14 +147,15 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa } } - removeRecentlyOpened(toRemove: URI[]): void { + removeRecentlyOpened(recentToRemove: URI[]): void { const keep = (recent: IRecent) => { const uri = location(recent); - for (const resource of toRemove) { - if (isEqual(resource, uri)) { + for (const resourceToRemove of recentToRemove) { + if (extUriBiasedIgnorePathCase.isEqual(resourceToRemove, uri)) { return false; } } + return true; }; @@ -319,8 +320,8 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa items: [ { type: 'task', - title: nls.localize('newWindow', "New Window"), - description: nls.localize('newWindowDesc', "Opens a new window"), + title: localize('newWindow', "New Window"), + description: localize('newWindowDesc', "Opens a new window"), program: process.execPath, args: '-n', // force new window iconPath: process.execPath, @@ -330,57 +331,53 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa }); // Recent Workspaces - try { - if (this.getRecentlyOpened().workspaces.length > 0) { + if (this.getRecentlyOpened().workspaces.length > 0) { - // The user might have meanwhile removed items from the jump list and we have to respect that - // so we need to update our list of recent paths with the choice of the user to not add them again - // Also: Windows will not show our custom category at all if there is any entry which was removed - // by the user! See https://github.com/microsoft/vscode/issues/15052 - let toRemove: URI[] = []; - for (let item of app.getJumpListSettings().removedItems) { - const args = item.args; - if (args) { - const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); - if (match) { - toRemove.push(URI.parse(match[2])); - } + // The user might have meanwhile removed items from the jump list and we have to respect that + // so we need to update our list of recent paths with the choice of the user to not add them again + // Also: Windows will not show our custom category at all if there is any entry which was removed + // by the user! See https://github.com/microsoft/vscode/issues/15052 + let toRemove: URI[] = []; + for (let item of app.getJumpListSettings().removedItems) { + const args = item.args; + if (args) { + const match = /^--(folder|file)-uri\s+"([^"]+)"$/.exec(args); + if (match) { + toRemove.push(URI.parse(match[2])); } } - this.removeRecentlyOpened(toRemove); - - // Add entries - jumpList.push({ - type: 'custom', - name: nls.localize('recentFolders', "Recent Workspaces"), - items: arrays.coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { - const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; - const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); - - let description; - let args; - if (isSingleFolderWorkspaceIdentifier(workspace)) { - description = nls.localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); - args = `--folder-uri "${workspace.toString()}"`; - } else { - description = nls.localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); - args = `--file-uri "${workspace.configPath.toString()}"`; - } - - return { - type: 'task', - title, - description, - program: process.execPath, - args, - iconPath: 'explorer.exe', // simulate folder icon - iconIndex: 0 - }; - })) - }); } - } catch (error) { - this.logService.warn('updateWindowsJumpList#recentWorkspaces', error); // https://github.com/microsoft/vscode/issues/111177 + this.removeRecentlyOpened(toRemove); + + // Add entries + jumpList.push({ + type: 'custom', + name: localize('recentFolders', "Recent Workspaces"), + items: coalesce(this.getRecentlyOpened().workspaces.slice(0, 7 /* limit number of entries here */).map(recent => { + const workspace = isRecentWorkspace(recent) ? recent.workspace : recent.folderUri; + const title = recent.label ? splitName(recent.label).name : this.getSimpleWorkspaceLabel(workspace, this.environmentService.untitledWorkspacesHome); + + let description; + let args; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + description = localize('folderDesc', "{0} {1}", getBaseLabel(workspace), getPathLabel(dirname(workspace), this.environmentService)); + args = `--folder-uri "${workspace.toString()}"`; + } else { + description = localize('workspaceDesc', "{0} {1}", getBaseLabel(workspace.configPath), getPathLabel(dirname(workspace.configPath), this.environmentService)); + args = `--file-uri "${workspace.configPath.toString()}"`; + } + + return { + type: 'task', + title: title.substr(0, 255), // Windows seems to be picky around the length of entries + description: description.substr(0, 255), // (see https://github.com/microsoft/vscode/issues/111177) + program: process.execPath, + args, + iconPath: 'explorer.exe', // simulate folder icon + iconIndex: 0 + }; + })) + }); } // Recent @@ -402,7 +399,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa // Workspace: Untitled if (extUriBiasedIgnorePathCase.isEqualOrParent(workspace.configPath, workspaceHome)) { - return nls.localize('untitledWorkspace', "Untitled (Workspace)"); + return localize('untitledWorkspace', "Untitled (Workspace)"); } let filename = basename(workspace.configPath); @@ -410,7 +407,7 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } - return nls.localize('workspaceName', "{0} (Workspace)", filename); + return localize('workspaceName', "{0} (Workspace)", filename); } } @@ -431,9 +428,9 @@ function indexOfWorkspace(arr: IRecent[], candidate: IWorkspaceIdentifier): numb } function indexOfFolder(arr: IRecent[], candidate: ISingleFolderWorkspaceIdentifier): number { - return arr.findIndex(folder => isRecentFolder(folder) && isEqual(folder.folderUri, candidate)); + return arr.findIndex(folder => isRecentFolder(folder) && extUriBiasedIgnorePathCase.isEqual(folder.folderUri, candidate)); } function indexOfFile(arr: IRecentFile[], candidate: URI): number { - return arr.findIndex(file => isEqual(file.fileUri, candidate)); + return arr.findIndex(file => extUriBiasedIgnorePathCase.isEqual(file.fileUri, candidate)); } diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index f4fa065fbb5..4663b2f7b3b 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -8,11 +8,11 @@ import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/e import { join, dirname } from 'vs/base/common/path'; import { mkdirp, writeFile, rimrafSync, readdirSync, writeFileSync } from 'vs/base/node/pfs'; import { readFileSync, existsSync, mkdirSync } from 'fs'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isWindows } from 'vs/base/common/platform'; import { Event, Emitter } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { createHash } from 'crypto'; -import * as json from 'vs/base/common/json'; +import { parse } from 'vs/base/common/json'; import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -25,8 +25,8 @@ import product from 'vs/platform/product/common/product'; import { MessageBoxOptions, BrowserWindow } from 'electron'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { findWindowOnWorkspace } from 'vs/platform/windows/node/window'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; +import { findWindowOnWorkspace } from 'vs/platform/windows/electron-main/window'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); @@ -66,7 +66,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain declare readonly _serviceBrand: undefined; - private readonly untitledWorkspacesHome: URI; // local URI that contains all untitled workspaces + private readonly untitledWorkspacesHome = this.environmentService.untitledWorkspacesHome; // local URI that contains all untitled workspaces private readonly _onUntitledWorkspaceDeleted = this._register(new Emitter()); readonly onUntitledWorkspaceDeleted: Event = this._onUntitledWorkspaceDeleted.event; @@ -81,8 +81,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain @IDialogMainService private readonly dialogMainService: IDialogMainService ) { super(); - - this.untitledWorkspacesHome = environmentService.untitledWorkspacesHome; } resolveLocalWorkspaceSync(uri: URI): IResolvedWorkspace | null { @@ -114,7 +112,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain return { id: workspaceIdentifier.id, configPath: workspaceIdentifier.configPath, - folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath), + folders: toWorkspaceFolders(workspace.folders, workspaceIdentifier.configPath, extUriBiasedIgnorePathCase), remoteAuthority: workspace.remoteAuthority }; } catch (error) { @@ -127,7 +125,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain private doParseStoredWorkspace(path: URI, contents: string): IStoredWorkspace { // Parse workspace file - let storedWorkspace: IStoredWorkspace = json.parse(contents); // use fault tolerant parser + const storedWorkspace: IStoredWorkspace = parse(contents); // use fault tolerant parser // Filter out folders which do not have a path or uri set if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { @@ -175,7 +173,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain const storedWorkspaceFolder: IStoredWorkspaceFolder[] = []; for (const folder of folders) { - storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder)); + storedWorkspaceFolder.push(getStoredWorkspaceFolder(folder.uri, true, folder.name, untitledWorkspaceConfigFolder, !isWindows, extUriBiasedIgnorePathCase)); } return { @@ -226,7 +224,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain } getUntitledWorkspacesSync(): IUntitledWorkspaceInfo[] { - let untitledWorkspaces: IUntitledWorkspaceInfo[] = []; + const untitledWorkspaces: IUntitledWorkspaceInfo[] = []; try { const untitledWorkspacePaths = readdirSync(this.untitledWorkspacesHome.fsPath).map(folder => joinPath(this.untitledWorkspacesHome, folder, UNTITLED_WORKSPACE_NAME)); for (const untitledWorkspacePath of untitledWorkspacePaths) { @@ -243,6 +241,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain this.logService.warn(`Unable to read folders in ${this.untitledWorkspacesHome} (${error}).`); } } + return untitledWorkspaces; } diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 07304aa32b0..303be42b5a6 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -17,8 +17,8 @@ import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; -import { dirname, joinPath } from 'vs/base/common/resources'; -import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { dirname, extUriBiasedIgnorePathCase, joinPath } from 'vs/base/common/resources'; +import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; @@ -325,7 +325,7 @@ suite('WorkspacesMainService', () => { let origConfigPath = URI.file(firstConfigPath); let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace')); - let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); let ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); // absolute path because outside of tmpdir @@ -334,7 +334,7 @@ suite('WorkspacesMainService', () => { origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); @@ -343,7 +343,7 @@ suite('WorkspacesMainService', () => { origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace')); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assertPathEquals((ws.folders[0]).path, folder1); @@ -352,7 +352,7 @@ suite('WorkspacesMainService', () => { origConfigPath = workspaceConfigPath; workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace'); - newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath); + newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); ws = (JSON.parse(newContent) as IStoredWorkspace); assert.equal(ws.folders.length, 3); assert.equal((ws.folders[0]).uri, URI.file(folder1).toString(true)); @@ -369,7 +369,7 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = `// this is a comment\n${origContent}`; - let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); assert.equal(0, newContent.indexOf('// this is a comment')); service.deleteUntitledWorkspaceSync(workspace); }); @@ -381,7 +381,7 @@ suite('WorkspacesMainService', () => { let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assert.ok(ws.folders.every(f => (f).path.indexOf('\\') < 0)); service.deleteUntitledWorkspaceSync(workspace); @@ -389,7 +389,7 @@ suite('WorkspacesMainService', () => { test.skip('rewriteWorkspaceFileForNewLocation (unc paths)', async () => { if (!isWindows) { - return Promise.resolve(); + return; } const workspaceLocation = path.join(os.tmpdir(), 'wsloc'); @@ -400,7 +400,7 @@ suite('WorkspacesMainService', () => { const workspace = await createUntitledWorkspace([folder1Location, folder2Location, folder3Location]); const workspaceConfigPath = URI.file(path.join(workspaceLocation, `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`)); let origContent = fs.readFileSync(workspace.configPath.fsPath).toString(); - const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath); + const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, false, workspaceConfigPath, extUriBiasedIgnorePathCase); const ws = (JSON.parse(newContent) as IStoredWorkspace); assertPathEquals((ws.folders[0]).path, folder1Location); assertPathEquals((ws.folders[1]).path, folder2Location); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 46671b11482..e1c2dd30ccb 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -8862,6 +8862,8 @@ declare module 'vscode' { * * Will only ever be called once per TreeItem. * + * onDidChangeTreeData should not be triggered from within resolveTreeItem. + * * *Note* that this function is called when tree items are already showing in the UI. * Because of that, no property that changes the presentation (label, description, command, etc.) * can be changed. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9609c022a7c..19e8d8f1032 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2307,12 +2307,12 @@ declare module 'vscode' { /** * The background color for this entry. * - * Note: the supported colors for background are currently limited to - * `ThemeColors` with id `statusBarItem.errorBackground`. Other `ThemeColors` - * will be ignored. + * Note: only `new ThemeColor('statusBarItem.errorBackground')` is + * supported for now. More background colors may be supported in the + * future. * - * When setting the background color to `statusBarItem.errorBackground`, it is - * recommended to also set `color` to `statusBarItem.errorForeground`. + * Note: when a background color is set, the statusbar may override + * the `color` choice to ensure the entry is readable in all themes. */ backgroundColor: ThemeColor | undefined; } diff --git a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts index 5e1331caaad..70fb045d7aa 100644 --- a/src/vs/workbench/api/browser/mainThreadBulkEdits.ts +++ b/src/vs/workbench/api/browser/mainThreadBulkEdits.ts @@ -39,6 +39,12 @@ export class MainThreadBulkEdits implements MainThreadBulkEditsShape { $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto, undoRedoGroupId?: number): Promise { const edits = reviveWorkspaceEditDto2(dto); - return this._bulkEditService.apply(edits, { undoRedoGroupId }).then(() => true, _err => false); + return this._bulkEditService.apply(edits, { + // having a undoRedoGroupId means that this is a nested workspace edit, + // e.g one from a onWill-handler and for now we need to forcefully suppress + // refactor previewing, see: https://github.com/microsoft/vscode/issues/111873#issuecomment-738739852 + undoRedoGroupId, + suppressPreview: typeof undoRedoGroupId === 'number' ? true : undefined + }).then(() => true, _err => false); } } diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 4a244875f42..d678f21bc67 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -10,22 +10,18 @@ import { IRemoteConsoleLog, log } from 'vs/base/common/console'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @extHostNamedCustomer(MainContext.MainThreadConsole) export class MainThreadConsole implements MainThreadConsoleShape { - private readonly _isExtensionDevHost: boolean; private readonly _isExtensionDevTestFromCli: boolean; constructor( - extHostContext: IExtHostContext, + _extHostContext: IExtHostContext, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @ILogService private readonly _logService: ILogService, - @IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService, ) { const devOpts = parseExtensionDevOptions(this._environmentService); - this._isExtensionDevHost = devOpts.isExtensionDevHost; this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli; } @@ -43,10 +39,5 @@ export class MainThreadConsole implements MainThreadConsoleShape { if (this._isExtensionDevTestFromCli) { logRemoteEntry(this._logService, entry); } - - // Broadcast to other windows if we are in development mode - else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { - this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); - } } } diff --git a/src/vs/workbench/api/browser/mainThreadDialogs.ts b/src/vs/workbench/api/browser/mainThreadDialogs.ts index 938cb20c18b..afa4240f39e 100644 --- a/src/vs/workbench/api/browser/mainThreadDialogs.ts +++ b/src/vs/workbench/api/browser/mainThreadDialogs.ts @@ -23,12 +23,20 @@ export class MainThreadDialogs implements MainThreadDiaglogsShape { // } - $showOpenDialog(options?: MainThreadDialogOpenOptions): Promise { - return Promise.resolve(this._fileDialogService.showOpenDialog(MainThreadDialogs._convertOpenOptions(options))); + async $showOpenDialog(options?: MainThreadDialogOpenOptions): Promise { + const convertedOptions = MainThreadDialogs._convertOpenOptions(options); + if (!convertedOptions.defaultUri) { + convertedOptions.defaultUri = await this._fileDialogService.defaultFilePath(); + } + return Promise.resolve(this._fileDialogService.showOpenDialog(convertedOptions)); } - $showSaveDialog(options?: MainThreadDialogSaveOptions): Promise { - return Promise.resolve(this._fileDialogService.showSaveDialog(MainThreadDialogs._convertSaveOptions(options))); + async $showSaveDialog(options?: MainThreadDialogSaveOptions): Promise { + const convertedOptions = MainThreadDialogs._convertSaveOptions(options); + if (!convertedOptions.defaultUri) { + convertedOptions.defaultUri = await this._fileDialogService.defaultFilePath(); + } + return Promise.resolve(this._fileDialogService.showSaveDialog(convertedOptions)); } private static _convertOpenOptions(options?: MainThreadDialogOpenOptions): IOpenDialogOptions { diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 51f47b691ff..8e32acd7f10 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -235,10 +235,14 @@ class TreeViewDataProvider implements ITreeViewDataProvider { private updateTreeItem(current: ITreeItem, treeItem: ITreeItem): void { treeItem.children = treeItem.children ? treeItem.children : undefined; if (current) { - const properties = distinct([...Object.keys(current), ...Object.keys(treeItem)]); + const properties = distinct([...Object.keys(current instanceof ResolvableTreeItem ? current.asTreeItem() : current), + ...Object.keys(treeItem)]); for (const property of properties) { (current)[property] = (treeItem)[property]; } + if (current instanceof ResolvableTreeItem) { + current.resetResolve(); + } } } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 328b9327207..2f3857ac7f3 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -35,6 +35,8 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService'; import { Emitter, Event } from 'vs/base/common/event'; import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; +import { isEqualOrParent } from 'vs/base/common/extpath'; +import { isLinux } from 'vs/base/common/platform'; interface ITestRunner { /** Old test runner API, as exported from `vscode/lib/testrunner` */ @@ -537,6 +539,20 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme const extensionTestsPath = originalFSPath(extensionTestsLocationURI); + let knowsExtension = false; + for (const extension of this._registry.getAllExtensionDescriptions()) { + if (extension.extensionLocation.scheme === Schemas.file) { + const extensionPath = originalFSPath(extension.extensionLocation); + if (isEqualOrParent(extensionTestsPath, extensionPath, !isLinux)) { + knowsExtension = true; + } + } + } + + if (!knowsExtension) { + return Promise.resolve(undefined); + } + // Require the test runner via node require from the provided path let testRunner: ITestRunner | INewTestRunner | undefined; let requireError: Error | undefined; diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index aa0b5df6cdb..a89e9fe3081 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -15,7 +15,15 @@ import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private static ID_GEN = 0; - private static ALLOWED_BACKGROUND_COLORS = new Set(['statusBarItem.errorBackground']); + + private static ALLOWED_BACKGROUND_COLORS = (() => { + const map = new Map(); + + // https://github.com/microsoft/vscode/issues/110214 + map.set('statusBarItem.errorBackground', new ThemeColor('statusBarItem.errorForeground')); + + return map; + })(); private _id: number; private _alignment: number; @@ -79,6 +87,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { } public get backgroundColor(): ThemeColor | undefined { + if (this._extension) { + checkProposedApiEnabled(this._extension); + } + return this._backgroundColor; } @@ -167,9 +179,15 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; + // If a background color is set, the foreground is determined + let color = this._color; + if (this._backgroundColor) { + color = ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id); + } + // Set to status bar - this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this._command?.internal, this.color, - this.backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, + this._proxy.$setEntry(this.id, this._statusId, this._statusName, this._text, this._tooltip, this._command?.internal, color, + this._backgroundColor, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority, this._accessibilityInformation); }, 0); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 2053893faac..d80dfae4b0c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -274,8 +274,8 @@ export namespace MarkdownString { if (isCodeblock(markup)) { const { language, value } = markup; res = { value: '```' + language + '\n' + value + '\n```\n' }; - } else if (htmlContent.isMarkdownString(markup)) { - res = markup; + } else if (types.MarkdownString.isMarkdownString(markup)) { + res = { value: markup.value, isTrusted: markup.isTrusted, supportThemeIcons: markup.supportThemeIcons }; } else if (typeof markup === 'string') { res = { value: markup }; } else { diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 922451d1079..ae477f9eaad 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -34,6 +34,92 @@ class ExtensionTunnel implements vscode.Tunnel { } } +export function getSockets(stdout: string): { pid: number, socket: number }[] { + const lines = stdout.trim().split('\n'); + const mapped: { pid: number, socket: number }[] = []; + lines.forEach(line => { + const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; + if (match && match.length >= 3) { + mapped.push({ + pid: parseInt(match[1], 10), + socket: parseInt(match[2], 10) + }); + } + }); + return mapped; +} + +export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { + const table = ([] as Record[]).concat(...stdouts.map(loadConnectionTable)); + return [ + ...new Map( + table.filter(row => row.st === '0A') + .map(row => { + const address = row.local_address.split(':'); + return { + socket: parseInt(row.inode, 10), + ip: parseIpAddress(address[0]), + port: parseInt(address[1], 16) + }; + }).map(port => [port.ip + ':' + port.port, port]) + ).values() + ]; +} + +export function parseIpAddress(hex: string): string { + let result = ''; + if (hex.length === 8) { + for (let i = hex.length - 2; i >= 0; i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + } else { + for (let i = hex.length - 4; i >= 0; i -= 4) { + result += parseInt(hex.substr(i, 4), 16).toString(16); + if (i !== 0) { + result += ':'; + } + } + } + return result; +} + +export function loadConnectionTable(stdout: string): Record[] { + const lines = stdout.trim().split('\n'); + const names = lines.shift()!.trim().split(/\s+/) + .filter(name => name !== 'rx_queue' && name !== 'tm->when'); + const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { + obj[names[i] || i] = value; + return obj; + }, {} as Record)); + return table; +} + +export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]) { + const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6); + const sockets = getSockets(procSockets); + + const socketMap = sockets.reduce((m, socket) => { + m[socket.socket] = socket; + return m; + }, {} as Record); + const processMap = processes.reduce((m, process) => { + m[process.pid] = process; + return m; + }, {} as Record); + + const ports: { host: string, port: number, detail: string }[] = []; + connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { + const command = processMap[socketMap[socket].pid].cmd; + if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) { + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); + } + }); + return ports; +} + export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService { readonly _serviceBrand: undefined; private readonly _proxy: MainThreadTunnelServiceShape; @@ -137,9 +223,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe return undefined; } - async findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { - const ports: { host: string, port: number, detail: string }[] = []; let tcp: string = ''; let tcp6: string = ''; try { @@ -170,89 +254,6 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe // } } - - const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6); - const sockets = this.getSockets(procSockets); - - const socketMap = sockets.reduce((m, socket) => { - m[socket.socket] = socket; - return m; - }, {} as Record); - const processMap = processes.reduce((m, process) => { - m[process.pid] = process; - return m; - }, {} as Record); - - connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { - const command = processMap[socketMap[socket].pid].cmd; - if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); - } - }); - - return ports; - } - - private getSockets(stdout: string): { pid: number, socket: number }[] { - const lines = stdout.trim().split('\n'); - const mapped: { pid: number, socket: number }[] = []; - lines.forEach(line => { - const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!; - if (match && match.length >= 3) { - mapped.push({ - pid: parseInt(match[1], 10), - socket: parseInt(match[2], 10) - }); - } - }); - return mapped; - } - - private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] { - const table = ([] as Record[]).concat(...stdouts.map(this.loadConnectionTable)); - return [ - ...new Map( - table.filter(row => row.st === '0A') - .map(row => { - const address = row.local_address.split(':'); - return { - socket: parseInt(row.inode, 10), - ip: this.parseIpAddress(address[0]), - port: parseInt(address[1], 16) - }; - }).map(port => [port.ip + ':' + port.port, port]) - ).values() - ]; - } - - private parseIpAddress(hex: string): string { - let result = ''; - if (hex.length === 8) { - for (let i = hex.length - 2; i >= 0; i -= 2) { - result += parseInt(hex.substr(i, 2), 16); - if (i !== 0) { - result += '.'; - } - } - } else { - for (let i = hex.length - 4; i >= 0; i -= 4) { - result += parseInt(hex.substr(i, 4), 16).toString(16); - if (i !== 0) { - result += ':'; - } - } - } - return result; - } - - private loadConnectionTable(stdout: string): Record[] { - const lines = stdout.trim().split('\n'); - const names = lines.shift()!.trim().split(/\s+/) - .filter(name => name !== 'rx_queue' && name !== 'tm->when'); - const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => { - obj[names[i] || i] = value; - return obj; - }, {} as Record)); - return table; + return findPorts(tcp, tcp6, procSockets, processes); } } diff --git a/src/vs/workbench/browser/actions/listCommands.ts b/src/vs/workbench/browser/actions/listCommands.ts index 5c8d8c70d1e..b71489ac38e 100644 --- a/src/vs/workbench/browser/actions/listCommands.ts +++ b/src/vs/workbench/browser/actions/listCommands.ts @@ -356,19 +356,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // List if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusPreviousPage(); - list.reveal(list.getFocus()[0]); + focused.focusPreviousPage(); } // Tree else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusPreviousPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); + focused.focusPreviousPage(fakeKeyboardEvent); } // Ensure DOM Focus @@ -386,19 +380,13 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ // List if (focused instanceof List || focused instanceof PagedList) { - const list = focused; - - list.focusNextPage(); - list.reveal(list.getFocus()[0]); + focused.focusNextPage(); } // Tree else if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) { - const list = focused; - const fakeKeyboardEvent = new KeyboardEvent('keydown'); - list.focusNextPage(fakeKeyboardEvent); - list.reveal(list.getFocus()[0]); + focused.focusNextPage(fakeKeyboardEvent); } // Ensure DOM Focus diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 92794740270..7f43e0fb0e5 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -131,7 +131,7 @@ interface IOpenFolderAPICommandOptions { CommandsRegistry.registerCommand({ id: 'vscode.openFolder', - handler: (accessor: ServicesAccessor, uri?: URI, arg: boolean | IOpenFolderAPICommandOptions = {}) => { + handler: (accessor: ServicesAccessor, uri?: URI, arg?: boolean | IOpenFolderAPICommandOptions) => { const commandService = accessor.get(ICommandService); // Be compatible to previous args by converting to options @@ -141,15 +141,15 @@ CommandsRegistry.registerCommand({ // Without URI, ask to pick a folder or workpsace to open if (!uri) { - return commandService.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg.forceNewWindow }); + return commandService.executeCommand('_files.pickFolderAndOpen', { forceNewWindow: arg?.forceNewWindow }); } uri = URI.revive(uri); const options: IOpenWindowOptions = { - forceNewWindow: arg.forceNewWindow, - forceReuseWindow: arg.forceReuseWindow, - noRecentEntry: arg.noRecentEntry + forceNewWindow: arg?.forceNewWindow, + forceReuseWindow: arg?.forceReuseWindow, + noRecentEntry: arg?.noRecentEntry }; const uriToOpen: IWindowOpenable = (hasWorkspaceFileExtension(uri) || uri.scheme === Schemas.untitled) ? { workspaceUri: uri } : { folderUri: uri }; diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index baab134342e..962b92b73e1 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -737,7 +737,7 @@ export class CompositeDragAndDropObserver extends Disposable { if (callbacks.onDragEnd) { this._onDragEnd.event(e => { callbacks.onDragEnd!(e); - }); + }, this, disposableStore); } return this._register(disposableStore); } diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index dfab3bc5cfc..8e78cbccc27 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -162,7 +162,7 @@ class PartLayout { if (this.options && this.options.hasTitle) { titleSize = new Dimension(width, Math.min(height, PartLayout.TITLE_HEIGHT)); } else { - titleSize = new Dimension(0, 0); + titleSize = Dimension.None; } let contentWidth = width; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 67b1c29733b..fdb0a57ac9d 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -464,7 +464,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands. MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.SHOW_EDITORS_IN_GROUP, title: nls.localize('showOpenedEditors', "Show Opened Editors") }, group: '3_open', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: nls.localize('closeAll', "Close All") }, group: '5_close', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.CLOSE_SAVED_EDITORS_COMMAND_ID, title: nls.localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 }); -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.KEEP_EDITORS_COMMAND_ID, title: nls.localize('toggleKeepEditors', "Keep Editors Open") }, when: ContextKeyExpr.has('config.workbench.editor.enablePreview'), group: '7_settings', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands.TOGGLE_KEEP_EDITORS_COMMAND_ID, title: nls.localize('toggleKeepEditors', "Keep Editors Open"), toggled: ContextKeyExpr.not('config.workbench.editor.enablePreview') }, group: '7_settings', order: 10 }); interface IEditorToolItem { id: string; title: string; icon?: { dark?: URI; light?: URI; } | ThemeIcon; } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index c776c18a2f2..dcece5343cb 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -40,7 +40,7 @@ export const CLOSE_OTHER_EDITORS_IN_GROUP_COMMAND_ID = 'workbench.action.closeOt export const MOVE_ACTIVE_EDITOR_COMMAND_ID = 'moveActiveEditor'; export const LAYOUT_EDITOR_GROUPS_COMMAND_ID = 'layoutEditorGroups'; export const KEEP_EDITOR_COMMAND_ID = 'workbench.action.keepEditor'; -export const KEEP_EDITORS_COMMAND_ID = 'workbench.action.keepEditors'; +export const TOGGLE_KEEP_EDITORS_COMMAND_ID = 'workbench.action.toggleKeepEditors'; export const SHOW_EDITORS_IN_GROUP = 'workbench.action.showEditorsInGroup'; export const PIN_EDITOR_COMMAND_ID = 'workbench.action.pinEditor'; @@ -899,19 +899,23 @@ function registerOtherEditorCommands(): void { }); CommandsRegistry.registerCommand({ - id: KEEP_EDITORS_COMMAND_ID, + id: TOGGLE_KEEP_EDITORS_COMMAND_ID, handler: accessor => { const configurationService = accessor.get(IConfigurationService); const notificationService = accessor.get(INotificationService); const openerService = accessor.get(IOpenerService); // Update setting - configurationService.updateValue('workbench.editor.enablePreview', false); + const currentSetting = configurationService.getValue('workbench.editor.enablePreview'); + const newSetting = currentSetting === true ? false : true; + configurationService.updateValue('workbench.editor.enablePreview', newSetting); // Inform user notificationService.prompt( Severity.Info, - nls.localize('disablePreview', "Preview editors have been disabled in settings."), + newSetting ? + nls.localize('enablePreview', "Preview editors have been enabled in settings.") : + nls.localize('disablePreview', "Preview editors have been disabled in settings."), [{ label: nls.localize('learnMode', "Learn More"), run: () => openerService.open('https://go.microsoft.com/fwlink/?linkid=2147473') }] diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index a212456dd4f..ed48c489007 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1704,13 +1704,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { layout(width: number, height: number): void { this.dimension = new Dimension(width, height); - // Ensure editor container gets height as CSS depending on the preferred height of the title control - const titleHeight = this.titleDimensions.height; - const editorHeight = Math.max(0, height - titleHeight); - this.editorContainer.style.height = `${editorHeight}px`; + // Layout the title area first to receive the size it occupies + const titleAreaSize = this.titleAreaControl.layout({ + container: this.dimension, + available: new Dimension(width, height - this.editorControl.minimumHeight) + }); - // Forward to controls - this.titleAreaControl.layout(new Dimension(width, titleHeight)); + // Pass the container width and remaining height to the editor layout + const editorHeight = Math.max(0, height - titleAreaSize.height); + this.editorContainer.style.height = `${editorHeight}px`; this.editorControl.layout(new Dimension(width, editorHeight)); } diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index a9158b796e4..637279fe48c 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -3,6 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/* + ################################### z-index explainer ################################### + + Tabs have various levels of z-index depending on state, typically: + - scrollbar should be above all + - sticky (compact, shrink) tabs need to be above non-sticky tabs for scroll under effect + including non-sticky tabs-top borders, otherwise these borders would not scroll under + (https://github.com/microsoft/vscode/issues/111641) + - bottom-border needs to be above tabs bottom border to win but also support sticky tabs + (https://github.com/microsoft/vscode/issues/99084) <- this currently cannot be done and + is still broken. putting sticky-tabs above tabs bottom border would not render this + border at all for sticky tabs. + + On top of that there is 2 borders with a z-index for a general border below tabs + - tabs bottom border + - editor title bottom border (when breadcrumbs are disabled, this border will appear + same as tabs bottom border) + + The following tabls shows the current stacking order: + + [z-index] [kind] + 7 scrollbar + 6 active-tab border-bottom + 5 tabs, title border bottom + 4 sticky-tab + 2 active/dirty-tab border top + 0 tab + + ########################################################################################## +*/ + /* Title Container */ .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { @@ -30,7 +61,7 @@ } .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar { - z-index: 3; /* on top of tabs */ + z-index: 7; cursor: default; } @@ -89,7 +120,7 @@ /** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */ position: sticky; - z-index: 1; + z-index: 4; /** Sticky compact/shrink tabs are even and never grow */ flex-basis: 0; @@ -167,24 +198,26 @@ display: block; position: absolute; left: 0; - z-index: 6; /* over possible title border */ pointer-events: none; width: 100%; } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 1px; background-color: var(--tab-border-top-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container { + z-index: 6; bottom: 0; height: 1px; background-color: var(--tab-border-bottom-color); } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container { + z-index: 2; top: 0; height: 2px; background-color: var(--tab-dirty-border-top-color); diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index 66a7caec173..35f36592e4a 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/notabstitlecontrol'; import { EditorResourceAccessor, Verbosity, IEditorInput, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; -import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl'; +import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl'; import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -333,9 +333,11 @@ export class NoTabsTitleControl extends TitleControl { }; } - layout(dimension: Dimension): void { + layout(dimensions: ITitleControlDimensions): Dimension { if (this.breadcrumbsControl) { this.breadcrumbsControl.layout(undefined); } + + return new Dimension(dimensions.container.width, this.getDimensions().height); } } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index bcd835f4cc5..556e4ffecbe 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -18,7 +18,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; +import { ITitleControlDimensions, TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IDisposable, dispose, DisposableStore, combinedDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -73,6 +73,9 @@ export class TabsTitleControl extends TitleControl { private static readonly TAB_HEIGHT = 35; + private static readonly MOUSE_WHEEL_EVENT_THRESHOLD = 150; + private static readonly MOUSE_WHEEL_DISTANCE_THRESHOLD = 1.5; + private titleContainer: HTMLElement | undefined; private tabsAndActionsContainer: HTMLElement | undefined; private tabsContainer: HTMLElement | undefined; @@ -87,12 +90,18 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimension: Dimension | undefined; + private dimensions: ITitleControlDimensions = { + container: Dimension.None, + available: Dimension.None + }; + private readonly layoutScheduled = this._register(new MutableDisposable()); private blockRevealActiveTab: boolean | undefined; private path: IPath = isWindows ? win32 : posix; + private lastMouseWheelEventTime = 0; + constructor( parent: HTMLElement, accessor: IEditorGroupsAccessor, @@ -337,8 +346,27 @@ export class TabsTitleControl extends TitleControl { } } - // Figure out scrolling direction - const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + (e.deltaX < 0 || e.deltaY < 0 /* scrolling up */ ? -1 : 1)); + // Ignore event if the last one happened too recently (https://github.com/microsoft/vscode/issues/96409) + // The restriction is relaxed according to the absolute value of `deltaX` and `deltaY` + // to support discrete (mouse wheel) and contiguous scrolling (touchpad) equally well + const now = Date.now(); + if (now - this.lastMouseWheelEventTime < TabsTitleControl.MOUSE_WHEEL_EVENT_THRESHOLD - 2 * (Math.abs(e.deltaX) + Math.abs(e.deltaY))) { + return; + } + + this.lastMouseWheelEventTime = now; + + // Figure out scrolling direction but ignore it if too subtle + let tabSwitchDirection: number; + if (e.deltaX + e.deltaY < - TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = -1; + } else if (e.deltaX + e.deltaY > TabsTitleControl.MOUSE_WHEEL_DISTANCE_THRESHOLD) { + tabSwitchDirection = 1; + } else { + return; + } + + const nextEditor = this.group.getEditorByIndex(this.group.getIndexOfEditor(activeEditor) + tabSwitchDirection); if (!nextEditor) { return; } @@ -356,7 +384,7 @@ export class TabsTitleControl extends TitleControl { // Changing the actions in the toolbar can have an impact on the size of the // tab container, so we need to layout the tabs to make sure the active is visible - this.layout(this.dimension); + this.layout(this.dimensions); } openEditor(editor: IEditorInput): void { @@ -439,7 +467,7 @@ export class TabsTitleControl extends TitleControl { }); // Moving an editor requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } pinEditor(editor: IEditorInput): void { @@ -466,7 +494,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to the sticky state requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } setActive(isGroupActive: boolean): void { @@ -478,7 +506,7 @@ export class TabsTitleControl extends TitleControl { // Activity has an impact on the toolbar, so we need to update and layout this.updateEditorActionsToolbar(); - this.layout(this.dimension); + this.layout(this.dimensions); } private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); @@ -504,7 +532,7 @@ export class TabsTitleControl extends TitleControl { }); // A change to a label requires a layout to keep the active editor visible - this.layout(this.dimension); + this.layout(this.dimensions); } updateEditorDirty(editor: IEditorInput): void { @@ -1022,7 +1050,7 @@ export class TabsTitleControl extends TitleControl { this.updateEditorActionsToolbar(); // Ensure the active tab is always revealed - this.layout(this.dimension); + this.layout(this.dimensions); } private redrawTab(editor: IEditorInput, index: number, tabContainer: HTMLElement, tabLabelWidget: IResourceLabel, tabLabel: IEditorInputLabel, tabActionBar: ActionBar): void { @@ -1236,7 +1264,7 @@ export class TabsTitleControl extends TitleControl { getDimensions(): IEditorGroupTitleDimensions { let height = TabsTitleControl.TAB_HEIGHT; if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - height += BreadcrumbsControl.HEIGHT; + height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible } return { @@ -1245,12 +1273,14 @@ export class TabsTitleControl extends TitleControl { }; } - layout(dimension: Dimension | undefined): void { - this.dimension = dimension; + layout(dimensions: ITitleControlDimensions): Dimension { + this.dimensions = dimensions; + // We need an opened editor and dimensions to layout the title + // Otherwise quickly return from the layout algorithm const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex || !this.dimension) { - return; + if (!activeTabAndIndex || dimensions.container === Dimension.None || dimensions.available === Dimension.None) { + return Dimension.None; } // The layout of tabs can be an expensive operation because we access DOM properties @@ -1258,34 +1288,32 @@ export class TabsTitleControl extends TitleControl { // this a little bit we try at least to schedule this work on the next animation frame. if (!this.layoutScheduled.value) { this.layoutScheduled.value = scheduleAtNextAnimationFrame(() => { - const dimension = assertIsDefined(this.dimension); - this.doLayout(dimension); + this.doLayout(this.dimensions); this.layoutScheduled.clear(); }); } + + return new Dimension(dimensions.container.width, this.getDimensions().height); } - private doLayout(dimension: Dimension): void { + private doLayout(dimensions: ITitleControlDimensions): void { const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; if (!activeTabAndIndex) { return; // nothing to do if not editor opened } // Breadcrumbs - this.doLayoutBreadcrumbs(dimension); + this.doLayoutBreadcrumbs(dimensions); // Tabs const [activeTab, activeIndex] = activeTabAndIndex; this.doLayoutTabs(activeTab, activeIndex); } - private doLayoutBreadcrumbs(dimension: Dimension): void { + private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { - const tabsScrollbar = assertIsDefined(this.tabsScrollbar); - - this.breadcrumbsControl.layout(new Dimension(dimension.width, BreadcrumbsControl.HEIGHT)); - tabsScrollbar.getDomNode().style.height = `${dimension.height - BreadcrumbsControl.HEIGHT}px`; + this.breadcrumbsControl.layout(new Dimension(dimensions.container.width, BreadcrumbsControl.HEIGHT)); } } @@ -1428,8 +1456,10 @@ export class TabsTitleControl extends TitleControl { const editorIndex = this.group.getIndexOfEditor(editor); if (editorIndex >= 0) { const tabsContainer = assertIsDefined(this.tabsContainer); - - return [tabsContainer.children[editorIndex] as HTMLElement, editorIndex]; + const tab = tabsContainer.children[editorIndex]; + if (tab) { + return [tab as HTMLElement, editorIndex]; + } } return undefined; diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8358dfcbaac..0eaa213ab76 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -46,6 +46,20 @@ export interface IToolbarActions { secondary: IAction[]; } +export interface ITitleControlDimensions { + + /** + * The size of the parent container the title control is layed out in. + */ + container: Dimension; + + /** + * The maximum size the title control is allowed to consume based on + * other controls that are positioned inside the container. + */ + available: Dimension; +} + export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); @@ -407,7 +421,7 @@ export abstract class TitleControl extends Themable { abstract updateStyles(): void; - abstract layout(dimension: Dimension): void; + abstract layout(dimensions: ITitleControlDimensions): Dimension; abstract getDimensions(): IEditorGroupTitleDimensions; diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 139b4530057..99fcf2b6ebe 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -11,7 +11,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAction, Action, SubmenuAction, Separator } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { isMacintosh, isWeb, isIOS } from 'vs/base/common/platform'; +import { isMacintosh, isWeb, isIOS, isNative } from 'vs/base/common/platform'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -199,13 +199,27 @@ export abstract class MenubarControl extends Disposable { if (event.affectsConfiguration('editor.accessibilitySupport')) { this.notifyUserOfCustomMenubarAccessibility(); } + + // Since we try not update when hidden, we should + // try to update the recently opened list on visibility changes + if (event.affectsConfiguration('window.menuBarVisibility')) { + this.onRecentlyOpenedChange(); + } + } + + private get menubarHidden(): boolean { + return isMacintosh && isNative ? false : getMenuBarVisibility(this.configurationService) === 'hidden'; } protected onRecentlyOpenedChange(): void { - this.workspacesService.getRecentlyOpened().then(recentlyOpened => { - this.recentlyOpened = recentlyOpened; - this.updateMenubar(); - }); + + // Do not update recently opened when the menubar is hidden #108712 + if (!this.menubarHidden) { + this.workspacesService.getRecentlyOpened().then(recentlyOpened => { + this.recentlyOpened = recentlyOpened; + this.updateMenubar(); + }); + } } private createOpenRecentMenuAction(recent: IRecent): IAction & { uri: URI } { diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 7ff3318d576..8f2df0476e3 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -68,6 +68,11 @@ margin-right: auto; } +.monaco-workbench .pane > .pane-body.wide > .welcome-view .monaco-button { + margin-left: inherit; + max-width: 260px; +} + .monaco-workbench .pane > .pane-body .welcome-view-content { padding: 0 20px 0 20px; box-sizing: border-box; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 2d1654ec47a..dbd8ffce1a9 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -880,10 +880,12 @@ class TreeRenderer extends Disposable implements ITreeRenderer(async (resolve) => { - await node.resolve(); - resolve(node.tooltip); - }), + markdown: (): Promise => { + return new Promise(async (resolve) => { + await node.resolve(); + resolve(node.tooltip); + }); + }, markdownNotSupportedFallback: resource ? undefined : '' // Passing undefined as the fallback for a resource falls back to the old native hover }; } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index e7d260b00ce..20df4472969 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -24,7 +24,7 @@ import { IFileService, IFileSystemProvider } from 'vs/platform/files/common/file import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { onUnexpectedError } from 'vs/base/common/errors'; import { setFullscreen } from 'vs/base/browser/browser'; import { isIOS, isMacintosh } from 'vs/base/common/platform'; @@ -158,7 +158,7 @@ class BrowserMain extends Disposable { }, undefined, isMacintosh ? 2000 /* adjust for macOS animation */ : 800 /* can be throttled */)); } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { + private async initServices(): Promise<{ serviceCollection: ServiceCollection, configurationService: IWorkbenchConfigurationService, logService: ILogService, storageService: BrowserStorageService }> { const serviceCollection = new ServiceCollection(); // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -212,7 +212,7 @@ class BrowserMain extends Disposable { serviceCollection.set(IWorkspaceContextService, service); // Configuration - serviceCollection.set(IConfigurationService, service); + serviceCollection.set(IWorkbenchConfigurationService, service); return service; }), diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 08b11a95553..0744cecf620 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -704,6 +704,27 @@ export class ResolvableTreeItem implements ITreeItem { get hasResolve(): boolean { return this._hasResolve; } + public resetResolve() { + this.resolved = false; + } + public asTreeItem(): ITreeItem { + return { + handle: this.handle, + parentHandle: this.parentHandle, + collapsibleState: this.collapsibleState, + label: this.label, + description: this.description, + icon: this.icon, + iconDark: this.iconDark, + themeIcon: this.themeIcon, + resourceUri: this.resourceUri, + tooltip: this.tooltip, + contextValue: this.contextValue, + command: this.command, + children: this.children, + accessibilityInformation: this.accessibilityInformation + }; + } } export interface ITreeViewDataProvider { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index ca55a105790..ffc54667e07 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -139,7 +139,7 @@ export class BulkEditService implements IBulkEditService { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + if (this._previewHandler && !options?.suppressPreview && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { edits = await this._previewHandler(edits, options); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index e2e5a774dc2..f4ac30ca099 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -93,8 +93,9 @@ class CopyOperation implements IFileOperation { return new Noop(); // not overwriting, but ignoring, and the target file exists } - await this._workingCopyFileService.copy([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); - return this._instaService.createInstance(DeleteOperation, this.newUri, this.options, { isUndoing: true }, true); + const stat = await this._workingCopyFileService.copy([{ source: this.oldUri, target: this.newUri }], { overwrite: this.options.overwrite, ...this.undoRedoInfo }, token); + const folder = this.options.folder || (stat.length === 1 && stat[0].isDirectory); + return this._instaService.createInstance(DeleteOperation, this.newUri, { recursive: true, folder, ...this.options }, { isUndoing: true }, false); } toString(): string { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 97a55f3168f..ab0172eeca7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -298,7 +298,7 @@ export class BulkEditPane extends ViewPane { } } - private async _openElementAsEditor(e: IOpenEvent): Promise { + private async _openElementAsEditor(e: IOpenEvent): Promise { type Mutable = { -readonly [P in keyof T]: T[P] }; diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts index 29ce65162d2..a0df024be67 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleWordWrap.ts @@ -10,10 +10,9 @@ import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { EditorOption, EditorOptions } from 'vs/editor/common/config/editorOptions'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -29,14 +28,7 @@ const isDominatedByLongLinesKey = 'isDominatedByLongLines'; * State written/read by the toggle word wrap action and associated with a particular model. */ interface IWordWrapTransientState { - readonly forceWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; - readonly forceWordWrapMinified: boolean; -} - -interface IWordWrapState { - readonly configuredWordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded' | undefined; - readonly configuredWordWrapMinified: boolean; - readonly transientState: IWordWrapTransientState | null; + readonly wordWrapOverride: 'on' | 'off'; } /** @@ -49,70 +41,10 @@ export function writeTransientState(model: ITextModel, state: IWordWrapTransient /** * Read (in memory) the word wrap state for a particular model. */ -function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState { +function readTransientState(model: ITextModel, codeEditorService: ICodeEditorService): IWordWrapTransientState | null { return codeEditorService.getTransientModelProperty(model, transientWordWrapState); } -function readWordWrapState(model: ITextModel, configurationService: ITextResourceConfigurationService, codeEditorService: ICodeEditorService): IWordWrapState { - const editorConfig = configurationService.getValue(model.uri, 'editor') as { wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded'; wordWrapMinified: boolean }; - let _configuredWordWrap = editorConfig && (typeof editorConfig.wordWrap === 'string' || typeof editorConfig.wordWrap === 'boolean') ? editorConfig.wordWrap : undefined; - - // Compatibility with old true or false values - if (_configuredWordWrap === true) { - _configuredWordWrap = 'on'; - } else if (_configuredWordWrap === false) { - _configuredWordWrap = 'off'; - } - - const _configuredWordWrapMinified = editorConfig && typeof editorConfig.wordWrapMinified === 'boolean' ? editorConfig.wordWrapMinified : undefined; - const _transientState = readTransientState(model, codeEditorService); - return { - configuredWordWrap: _configuredWordWrap, - configuredWordWrapMinified: (typeof _configuredWordWrapMinified === 'boolean' ? _configuredWordWrapMinified : EditorOptions.wordWrapMinified.defaultValue), - transientState: _transientState - }; -} - -function toggleWordWrap(editor: ICodeEditor, state: IWordWrapState): IWordWrapState { - if (state.transientState) { - // toggle off => go to null - return { - configuredWordWrap: state.configuredWordWrap, - configuredWordWrapMinified: state.configuredWordWrapMinified, - transientState: null - }; - } - - let transientState: IWordWrapTransientState; - - const actualWrappingInfo = editor.getOption(EditorOption.wrappingInfo); - if (actualWrappingInfo.isWordWrapMinified) { - // => wrapping due to minified file - transientState = { - forceWordWrap: 'off', - forceWordWrapMinified: false - }; - } else if (state.configuredWordWrap !== 'off') { - // => wrapping is configured to be on (or some variant) - transientState = { - forceWordWrap: 'off', - forceWordWrapMinified: false - }; - } else { - // => wrapping is configured to be off - transientState = { - forceWordWrap: 'on', - forceWordWrapMinified: state.configuredWordWrapMinified - }; - } - - return { - configuredWordWrap: state.configuredWordWrap, - configuredWordWrapMinified: state.configuredWordWrapMinified, - transientState: transientState - }; -} - const TOGGLE_WORD_WRAP_ID = 'editor.action.toggleWordWrap'; class ToggleWordWrapAction extends EditorAction { @@ -139,7 +71,6 @@ class ToggleWordWrapAction extends EditorAction { return; } - const textResourceConfigurationService = accessor.get(ITextResourceConfigurationService); const codeEditorService = accessor.get(ICodeEditorService); const model = editor.getModel(); @@ -148,12 +79,21 @@ class ToggleWordWrapAction extends EditorAction { } // Read the current state - const currentState = readWordWrapState(model, textResourceConfigurationService, codeEditorService); + const transientState = readTransientState(model, codeEditorService); + // Compute the new state - const newState = toggleWordWrap(editor, currentState); + let newState: IWordWrapTransientState | null; + if (transientState) { + newState = null; + } else { + const actualWrappingInfo = editor.getOption(EditorOption.wrappingInfo); + const wordWrapOverride = (actualWrappingInfo.wrappingColumn === -1 ? 'on' : 'off'); + newState = { wordWrapOverride }; + } + // Write the new state // (this will cause an event and the controller will apply the state) - writeTransientState(model, newState.transientState, codeEditorService); + writeTransientState(model, newState, codeEditorService); } } @@ -162,24 +102,23 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution public static readonly ID = 'editor.contrib.toggleWordWrapController'; constructor( - private readonly editor: ICodeEditor, - @IContextKeyService readonly contextKeyService: IContextKeyService, - @ITextResourceConfigurationService readonly configurationService: ITextResourceConfigurationService, - @ICodeEditorService readonly codeEditorService: ICodeEditorService + private readonly _editor: ICodeEditor, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(); - const options = this.editor.getOptions(); + const options = this._editor.getOptions(); const wrappingInfo = options.get(EditorOption.wrappingInfo); - const isWordWrapMinified = this.contextKeyService.createKey(isWordWrapMinifiedKey, wrappingInfo.isWordWrapMinified); - const isDominatedByLongLines = this.contextKeyService.createKey(isDominatedByLongLinesKey, wrappingInfo.isDominatedByLongLines); + const isWordWrapMinified = this._contextKeyService.createKey(isWordWrapMinifiedKey, wrappingInfo.isWordWrapMinified); + const isDominatedByLongLines = this._contextKeyService.createKey(isDominatedByLongLinesKey, wrappingInfo.isDominatedByLongLines); let currentlyApplyingEditorConfig = false; - this._register(editor.onDidChangeConfiguration((e) => { + this._register(_editor.onDidChangeConfiguration((e) => { if (!e.hasChanged(EditorOption.wrappingInfo)) { return; } - const options = this.editor.getOptions(); + const options = this._editor.getOptions(); const wrappingInfo = options.get(EditorOption.wrappingInfo); isWordWrapMinified.set(wrappingInfo.isWordWrapMinified); isDominatedByLongLines.set(wrappingInfo.isDominatedByLongLines); @@ -189,25 +128,25 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution } })); - this._register(editor.onDidChangeModel((e) => { + this._register(_editor.onDidChangeModel((e) => { ensureWordWrapSettings(); })); - this._register(codeEditorService.onDidChangeTransientModelProperty(() => { + this._register(_codeEditorService.onDidChangeTransientModelProperty(() => { ensureWordWrapSettings(); })); const ensureWordWrapSettings = () => { - if (this.editor.getContribution(DefaultSettingsEditorContribution.ID)) { + if (this._editor.getContribution(DefaultSettingsEditorContribution.ID)) { // in the settings editor... return; } - if (this.editor.isSimpleWidget) { + if (this._editor.isSimpleWidget) { // in a simple widget... return; } // Ensure correct word wrap settings - const newModel = this.editor.getModel(); + const newModel = this._editor.getModel(); if (!newModel) { return; } @@ -216,33 +155,22 @@ class ToggleWordWrapController extends Disposable implements IEditorContribution return; } - // Read current configured values and toggle state - const desiredState = readWordWrapState(newModel, this.configurationService, this.codeEditorService); + const transientState = readTransientState(newModel, this._codeEditorService); // Apply the state try { currentlyApplyingEditorConfig = true; - this._applyWordWrapState(desiredState); + this._applyWordWrapState(transientState); } finally { currentlyApplyingEditorConfig = false; } }; } - private _applyWordWrapState(state: IWordWrapState): void { - if (state.transientState) { - // toggle is on - this.editor.updateOptions({ - wordWrap: state.transientState.forceWordWrap, - wordWrapMinified: state.transientState.forceWordWrapMinified - }); - return; - } - - // toggle is off - this.editor.updateOptions({ - wordWrap: state.configuredWordWrap, - wordWrapMinified: state.configuredWordWrapMinified + private _applyWordWrapState(state: IWordWrapTransientState | null): void { + const wordWrapOverride2 = state ? state.wordWrapOverride : 'inherit'; + this._editor.updateOptions({ + wordWrapOverride2: wordWrapOverride2 }); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 8f8c4ffcfde..ffab7e07c3a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -951,7 +951,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const fontInfo = this.editor.getOption(EditorOption.fontInfo); content.push(`.monaco-editor .review-widget .body code { - font-family: ${fontInfo.fontFamily}; + font-family: '${fontInfo.fontFamily}'; font-size: ${fontInfo.fontSize}px; font-weight: ${fontInfo.fontWeight}; }`); diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5ed011a2c64..d917b578057 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/panel'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { basename, isEqual } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/resources'; import { IAction, Action } from 'vs/base/common/actions'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -28,6 +28,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export class CommentsPanel extends ViewPane { private treeLabels!: ResourceLabels; @@ -52,6 +53,7 @@ export class CommentsPanel extends ViewPane { @IThemeService themeService: IThemeService, @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } @@ -206,7 +208,7 @@ export class CommentsPanel extends ViewPane { const activeEditor = this.editorService.activeEditor; let currentActiveResource = activeEditor ? activeEditor.resource : undefined; - if (currentActiveResource && isEqual(currentActiveResource, element.resource)) { + if (this.uriIdentityService.extUri.isEqual(element.resource, currentActiveResource)) { const threadToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].threadId : element.threadId; const commentToReveal = element instanceof ResourceWithCommentThreads ? element.commentThreads[0].comment.uniqueIdInThread : element.comment.uniqueIdInThread; const control = this.editorService.activeTextEditorControl; diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index cfa7d955f41..da9c9f970d8 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -397,7 +397,7 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before { - content: "💬"; + content: "+"; } .monaco-editor .comment-range-glyph.comment-thread { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index c57458f49a5..7fa72c43fc6 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -124,7 +124,7 @@ export class BreakpointsView extends ViewPane { const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); this._register(resourceNavigator.onDidOpen(async e => { - if (e.element === null) { + if (!e.element) { return; } @@ -132,14 +132,12 @@ export class BreakpointsView extends ViewPane { return; } - const element = this.list.element(e.element); - - if (element instanceof Breakpoint) { - openBreakpointSource(element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); + if (e.element instanceof Breakpoint) { + openBreakpointSource(e.element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); } - if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedBreakpoint()) { + if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.debugService.getViewModel().getSelectedBreakpoint()) { // double click - this.debugService.getViewModel().setSelectedBreakpoint(element); + this.debugService.getViewModel().setSelectedBreakpoint(e.element); this.onBreakpointsChange(); } })); @@ -190,44 +188,47 @@ export class BreakpointsView extends ViewPane { const actions: IAction[] = []; const element = e.element; - const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint"); - if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { - actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, async () => { - if (element instanceof Breakpoint) { - const editor = await openBreakpointSource(element, false, false, true, this.debugService, this.editorService); - if (editor) { - const codeEditor = editor.getControl(); - if (isCodeEditor(codeEditor)) { - codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column); - } - } - } else { + if (element instanceof ExceptionBreakpoint) { + if (element.supportsCondition) { + actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition"), '', true, async () => { this.debugService.getViewModel().setSelectedBreakpoint(element); this.onBreakpointsChange(); - } - })); + })); + } + } else { + const breakpointType = element instanceof Breakpoint && element.logMessage ? nls.localize('Logpoint', "Logpoint") : nls.localize('Breakpoint', "Breakpoint"); + if (element instanceof Breakpoint || element instanceof FunctionBreakpoint) { + actions.push(new Action('workbench.action.debug.openEditorAndEditBreakpoint', nls.localize('editBreakpoint', "Edit {0}...", breakpointType), '', true, async () => { + if (element instanceof Breakpoint) { + const editor = await openBreakpointSource(element, false, false, true, this.debugService, this.editorService); + if (editor) { + const codeEditor = editor.getControl(); + if (isCodeEditor(codeEditor)) { + codeEditor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID).showBreakpointWidget(element.lineNumber, element.column); + } + } + } else { + this.debugService.getViewModel().setSelectedBreakpoint(element); + this.onBreakpointsChange(); + } + })); + actions.push(new Separator()); + } + + + actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); + + if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length >= 1) { + actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); + actions.push(new Separator()); + + actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); + actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); + } + actions.push(new Separator()); + actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService)); } - if (element instanceof ExceptionBreakpoint && element.supportsCondition) { - actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition..."), '', true, async () => { - this.debugService.getViewModel().setSelectedBreakpoint(element); - this.onBreakpointsChange(); - })); - actions.push(new Separator()); - } - - actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService)); - - if (this.debugService.getModel().getBreakpoints().length + this.debugService.getModel().getFunctionBreakpoints().length > 1) { - actions.push(new RemoveAllBreakpointsAction(RemoveAllBreakpointsAction.ID, RemoveAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new Separator()); - - actions.push(new EnableAllBreakpointsAction(EnableAllBreakpointsAction.ID, EnableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - actions.push(new DisableAllBreakpointsAction(DisableAllBreakpointsAction.ID, DisableAllBreakpointsAction.LABEL, this.debugService, this.keybindingService)); - } - - actions.push(new Separator()); - actions.push(new ReapplyBreakpointsAction(ReapplyBreakpointsAction.ID, ReapplyBreakpointsAction.LABEL, this.debugService, this.keybindingService)); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 9fd5d78b003..fa7901ea386 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -127,17 +127,21 @@ export class CallStackEditorContribution implements IEditorContribution { const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { - let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; - if (!candidateStackFrame) { - const callStack = t.getCallStack(); - if (callStack.length) { - candidateStackFrame = callStack[0]; + const callStack = t.getCallStack(); + const stackFrames: IStackFrame[] = []; + if (callStack.length > 0) { + // Always decorate top stack frame, and decorate focused stack frame if it is not the top stack frame + if (focusedStackFrame && !focusedStackFrame.equals(callStack[0])) { + stackFrames.push(focusedStackFrame); } + stackFrames.push(callStack[0]); } - if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); - } + stackFrames.forEach(candidateStackFrame => { + if (candidateStackFrame && this.uriIdentityService.extUri.isEqual(candidateStackFrame.source.uri, this.editor.getModel()?.uri)) { + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); + } + }); } }); }); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index d88475e263c..f37f9dd71d0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -339,7 +339,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (this.hoverRange) { this.showHover(this.hoverRange, false); } - }, hoverOption.delay); + }, hoverOption.delay * 2); this.toDispose.push(scheduler); return scheduler; @@ -452,9 +452,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { closeExceptionWidget(): void { if (this.exceptionWidget) { + const shouldFocusEditor = this.exceptionWidget.hasfocus(); this.exceptionWidget.dispose(); this.exceptionWidget = undefined; this.exceptionWidgetVisible.set(false); + if (shouldFocusEditor) { + this.editor.focus(); + } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index e05a471f57b..cca24fe1fc9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -25,7 +25,6 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { parse, getFirstFrame } from 'vs/base/common/console'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IAction, Action } from 'vs/base/common/actions'; @@ -149,16 +148,6 @@ export class DebugService implements IDebugService { session.disconnect(); } })); - this.toDispose.push(this.extensionHostDebugService.onLogToSession(event => { - const session = this.model.getSession(event.sessionId, true); - if (session) { - // extension logged output -> show it in REPL - const sev = event.log.severity === 'warn' ? severity.Warning : event.log.severity === 'error' ? severity.Error : severity.Info; - const { args, stack } = parse(event.log); - const frame = !!stack ? getFirstFrame(stack) : undefined; - session.logToRepl(sev, args, frame); - } - })); this.toDispose.push(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); @@ -290,6 +279,11 @@ export class DebugService implements IDebugService { await this.extensionService.activateByEvent('onDebug'); if (!options?.parentSession) { await this.editorService.saveAll(); + const activeEditor = this.editorService.activeEditorPane; + if (activeEditor) { + // Make sure to save the active editor in case it is in untitled file it wont be saved as part of saveAll #111850 + await this.editorService.save({ editor: activeEditor.input, groupId: activeEditor.group.id }); + } } await this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined); await this.extensionService.whenInstalledExtensionsRegistered(); diff --git a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts index cd8159ad070..67a12c4892c 100644 --- a/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/exceptionWidget.ts @@ -120,4 +120,8 @@ export class ExceptionWidget extends ZoneWidget { // Focus into the container for accessibility purposes so the exception and stack trace gets read this.container?.focus(); } + + hasfocus(): boolean { + return dom.isAncestor(document.activeElement, this.container); + } } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 16593506851..58e44a25ad6 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -305,7 +305,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; const fontSize = debugConsole.fontSize; - const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : debugConsole.fontFamily; + const fontFamily = debugConsole.fontFamily === 'default' ? 'var(--monaco-monospace-font)' : `${debugConsole.fontFamily}`; const lineHeight = debugConsole.lineHeight ? `${debugConsole.lineHeight}px` : '1.4em'; const backgroundColor = this.themeService.getColorTheme().getColor(this.getBackgroundColor()); @@ -559,7 +559,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), - new ReplGroupRenderer(), + this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), new ReplEvaluationResultsRenderer(linkDetector), new ReplRawObjectsRenderer(linkDetector), ], diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 0d432417fcf..01614150f9e 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -34,7 +34,7 @@ interface IReplEvaluationInputTemplateData { } interface IReplGroupTemplateData { - label: HighlightedLabel; + label: HTMLElement; } interface IReplEvaluationResultTemplateData { @@ -87,22 +87,28 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer { static readonly ID = 'replGroup'; + constructor( + private readonly linkDetector: LinkDetector, + @IThemeService private readonly themeService: IThemeService + ) { } + get templateId(): string { return ReplGroupRenderer.ID; } - renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { - const input = dom.append(container, $('.expression')); - const label = new HighlightedLabel(input, false); + renderTemplate(container: HTMLElement): IReplGroupTemplateData { + const label = dom.append(container, $('.expression')); return { label }; } renderElement(element: ITreeNode, _index: number, templateData: IReplGroupTemplateData): void { const replGroup = element.element; - templateData.label.set(replGroup.name, createMatches(element.filterData)); + dom.clearNode(templateData.label); + const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); + templateData.label.appendChild(result); } - disposeTemplate(_templateData: IReplEvaluationInputTemplateData): void { + disposeTemplate(_templateData: IReplGroupTemplateData): void { // noop } } diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index a00d0ce22bb..96c54f0a83a 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -361,7 +361,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { data.msgContainer.appendChild(el); } - if (element.description.extensionLocation.scheme !== Schemas.file) { + if (element.description.extensionLocation.scheme === Schemas.vscodeRemote) { const el = $('span', undefined, ...renderCodicons(`$(remote) ${element.description.extensionLocation.authority}`)); data.msgContainer.appendChild(el); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index a4b6d671ae3..7a58b96da62 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -31,7 +31,7 @@ import { UpdateAction, ReloadAction, MaliciousStatusLabelAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, ToggleSyncExtensionAction, SetProductIconThemeAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, UninstallAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, - InstallAnotherVersionAction, ExtensionEditorManageExtensionAction + InstallAnotherVersionAction, ExtensionEditorManageExtensionAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -422,6 +422,7 @@ export class ExtensionEditor extends EditorPane { this.instantiationService.createInstance(DisableDropDownAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), + this.instantiationService.createInstance(WebInstallAction), combinedInstallAction, this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(ActionWithDropDownAction, 'extensions.uninstall', UninstallAction.UninstallLabel, [ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 503be3e29f8..36dbf7c75ba 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -15,7 +15,7 @@ import { dispose } from 'vs/base/common/lifecycle'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate'; import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensioManagementService, IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; @@ -50,7 +50,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { prefersExecuteOnUI, prefersExecuteOnWorkspace, canExecuteOnUI, canExecuteOnWorkspace, prefersExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -150,7 +150,7 @@ class PromptExtensionInstallFailureAction extends Action { }) }); } - const checkLogsMessage = localize('check logs', "Please check [logs]({0}) for more details.", `command:${Constants.showWindowLogActionId}`); + const checkLogsMessage = localize('check logs', "Please check the [log]({0}) for more details.", `command:${Constants.showWindowLogActionId}`); this.notificationService.prompt(Severity.Error, `${operationMessage} ${checkLogsMessage}`, promptChoices); } } @@ -479,7 +479,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { } } - private canInstall(): boolean { + protected canInstall(): boolean { // Disable if extension is not installed or not an user extension if ( !this.extension @@ -506,6 +506,11 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { return true; } + // Prefers to run on Web + if (this.server === this.extensionManagementServerService.webExtensionManagementServer && prefersExecuteOnWeb(this.extension.local.manifest, this.productService, this.configurationService)) { + return true; + } + if (this.canInstallAnyWhere) { // Can run on UI if (this.server === this.extensionManagementServerService.localExtensionManagementServer && canExecuteOnUI(this.extension.local.manifest, this.productService, this.configurationService)) { @@ -577,6 +582,31 @@ export class LocalInstallAction extends InstallInOtherServerAction { } +export class WebInstallAction extends InstallInOtherServerAction { + + constructor( + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService, + ) { + super(`extensions.webInstall`, extensionManagementServerService.webExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService); + } + + protected getInstallLabel(): string { + return localize('install browser', "Install in Browser"); + } + + protected canInstall(): boolean { + if (super.canInstall()) { + return !!this.extension?.gallery && this.webExtensionsScannerService.canAddExtension(this.extension.gallery); + } + return false; + } + +} + export class UninstallAction extends ExtensionAction { static readonly UninstallLabel = localize('uninstallAction', "Uninstall"); @@ -1399,11 +1429,12 @@ export class ReloadAction extends ExtensionAction { } const isUninstalled = this.extension.state === ExtensionState.Uninstalled; - const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; - const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); + const runningExtension = this._runningExtensions.find(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier)); if (isUninstalled) { - if (isSameExtensionRunning && !this.extensionService.canRemoveExtension(runningExtension)) { + const canRemoveRunningExtension = runningExtension && this.extensionService.canRemoveExtension(runningExtension); + const isSameExtensionRunning = runningExtension && (!this.extension.server || this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension))); + if (!canRemoveRunningExtension && isSameExtensionRunning) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postUninstallTooltip', "Please reload Visual Studio Code to complete the uninstallation of this extension."); @@ -1412,6 +1443,7 @@ export class ReloadAction extends ExtensionAction { return; } if (this.extension.local) { + const isSameExtensionRunning = runningExtension && this.extension.server === this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)); const isEnabled = this.extensionEnablementService.isEnabled(this.extension.local); // Extension is running diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 6b0ed05167f..563a5534ac5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -14,7 +14,7 @@ import { IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { Event } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IExtension, ExtensionContainers, ExtensionState, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; -import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { UpdateAction, ManageExtensionAction, ReloadAction, MaliciousStatusLabelAction, ExtensionActionViewItem, StatusLabelAction, RemoteInstallAction, SystemDisabledWarningAction, ExtensionToolTipAction, LocalInstallAction, ActionWithDropDownAction, InstallDropdownAction, InstallingLabelAction, ExtensionActionWithDropdownActionViewItem, ExtensionDropDownAction, WebInstallAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { Label, RatingsWidget, InstallCountWidget, RecommendationWidget, RemoteBadgeWidget, TooltipWidget, ExtensionPackCountWidget as ExtensionPackBadgeWidget, SyncIgnoredWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; @@ -112,6 +112,7 @@ export class Renderer implements IPagedRenderer { this.instantiationService.createInstance(InstallingLabelAction), this.instantiationService.createInstance(RemoteInstallAction, false), this.instantiationService.createInstance(LocalInstallAction), + this.instantiationService.createInstance(WebInstallAction), this.instantiationService.createInstance(MaliciousStatusLabelAction, false), systemDisabledWarningAction, this.instantiationService.createInstance(ManageExtensionAction) diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 1466661faa5..932a76b68d6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -177,7 +177,7 @@ export class ExtensionsListView extends ViewPane { const resourceNavigator = this._register(new ListResourceNavigator(this.list, { openOnSingleClick: true })); this._register(Event.debounce(Event.filter(resourceNavigator.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => { - this.openExtension(this.list!.model.get(options.element!), { sideByside: options.sideBySide, ...options.editorOptions }); + this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions }); })); this.bodyTemplate = { diff --git a/src/vs/workbench/contrib/files/browser/explorerService.ts b/src/vs/workbench/contrib/files/browser/explorerService.ts index 6398736b046..a1a537578e0 100644 --- a/src/vs/workbench/contrib/files/browser/explorerService.ts +++ b/src/vs/workbench/contrib/files/browser/explorerService.ts @@ -21,13 +21,14 @@ import { UndoRedoSource } from 'vs/platform/undoRedo/common/undoRedo'; import { IExplorerView, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { RunOnceScheduler } from 'vs/base/common/async'; export const UNDO_REDO_SOURCE = new UndoRedoSource(); export class ExplorerService implements IExplorerService { declare readonly _serviceBrand: undefined; - private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 500; // delay in ms to react to file changes to give our internal events a chance to react first + private static readonly EXPLORER_FILE_CHANGES_REACT_DELAY = 1000; // delay in ms to react to file changes to give our internal events a chance to react first private readonly disposables = new DisposableStore(); private editable: { stat: ExplorerItem, data: IEditableData } | undefined; @@ -35,6 +36,8 @@ export class ExplorerService implements IExplorerService { private cutItems: ExplorerItem[] | undefined; private view: IExplorerView | undefined; private model: ExplorerModel; + private onFileChangesScheduler: RunOnceScheduler; + private fileChangeEvents: FileChangesEvent[] = []; constructor( @IFileService private fileService: IFileService, @@ -51,7 +54,36 @@ export class ExplorerService implements IExplorerService { this.model = new ExplorerModel(this.contextService, this.uriIdentityService, this.fileService); this.disposables.add(this.model); this.disposables.add(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); - this.disposables.add(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + + this.onFileChangesScheduler = new RunOnceScheduler(async () => { + const events = this.fileChangeEvents; + this.fileChangeEvents = []; + + // Filter to the ones we care + const types = [FileChangeType.ADDED, FileChangeType.DELETED]; + if (this._sortOrder === SortOrder.Modified) { + types.push(FileChangeType.UPDATED); + } + + let shouldRefresh = false; + this.roots.forEach(r => { + if (this.view && !shouldRefresh) { + shouldRefresh = doesFileEventAffect(r, this.view, events, types); + } + }); + + if (shouldRefresh) { + await this.refresh(false); + } + + }, ExplorerService.EXPLORER_FILE_CHANGES_REACT_DELAY); + + this.disposables.add(this.fileService.onDidFilesChange(e => { + this.fileChangeEvents.push(e); + if (!this.onFileChangesScheduler.isScheduled()) { + this.onFileChangesScheduler.schedule(); + } + })); this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(async e => { let affected = false; @@ -296,32 +328,6 @@ export class ExplorerService implements IExplorerService { } } - private onDidFilesChange(e: FileChangesEvent): void { - // Check if an explorer refresh is necessary (delayed to give internal events a chance to react first) - // Note: there is no guarantee when the internal events are fired vs real ones. Code has to deal with the fact that one might - // be fired first over the other or not at all. - setTimeout(async () => { - // Filter to the ones we care - const types = [FileChangeType.ADDED, FileChangeType.DELETED]; - if (this._sortOrder === SortOrder.Modified) { - types.push(FileChangeType.UPDATED); - } - - const allResolvedDirectories: ExplorerItem[] = []; - this.roots.forEach(r => { - allResolvedDirectories.push(r); - if (this.view) { - getAllNonFilteredDescendants(r, allResolvedDirectories, this.view); - } - }); - - const shouldRefresh = allResolvedDirectories.some(r => e.affects(r.resource, ...types)); - if (shouldRefresh) { - await this.refresh(false); - } - }, ExplorerService.EXPLORER_FILE_CHANGES_REACT_DELAY); - } - private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { const configSortOrder = configuration?.explorer?.sortOrder || 'default'; if (this._sortOrder !== configSortOrder) { @@ -338,13 +344,19 @@ export class ExplorerService implements IExplorerService { } } -function getAllNonFilteredDescendants(item: ExplorerItem, result: ExplorerItem[], view: IExplorerView): void { +function doesFileEventAffect(item: ExplorerItem, view: IExplorerView, events: FileChangesEvent[], types: FileChangeType[]): boolean { + if (events.some(e => e.affects(item.resource, ...types))) { + return true; + } for (let [_name, child] of item.children) { if (view.isItemVisible(child)) { if (child.isDirectory && child.isDirectoryResolved) { - result.push(child); - getAllNonFilteredDescendants(child, result, view); + if (doesFileEventAffect(child, view, events, types)) { + return true; + } } } } + + return false; } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 85ae6b84699..46aacd31f37 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -914,8 +914,8 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole try { const resourceToCreate = resources.joinPath(folder.resource, value); await explorerService.applyBulkEdit([new ResourceFileEdit(undefined, resourceToCreate, { folder: isFolder })], { - progressLabel: nls.localize('newBulkEdit', "New {0}", value), - undoLabel: nls.localize('newBulkEdit', "New {0}", value) + undoLabel: nls.localize('createBulkEdit', "Create {0}", value), + progressLabel: nls.localize('creatingBulkEdit', "Creating {0}", value) }); await refreshIfSeparator(value, explorerService); @@ -1025,7 +1025,7 @@ export const cutFileHandler = async (accessor: ServicesAccessor) => { }; export const DOWNLOAD_COMMAND_ID = 'explorer.download'; -const downloadFileHandler = (accessor: ServicesAccessor) => { +const downloadFileHandler = async (accessor: ServicesAccessor) => { const logService = accessor.get(ILogService); const fileService = accessor.get(IFileService); const fileDialogService = accessor.get(IFileDialogService); @@ -1037,7 +1037,7 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { const cts = new CancellationTokenSource(); - const downloadPromise = progressService.withProgress({ + await progressService.withProgress({ location: ProgressLocation.Window, delay: 800, cancellable: isWeb, @@ -1257,9 +1257,6 @@ const downloadFileHandler = (accessor: ServicesAccessor) => { } })); }, () => cts.dispose(true)); - - // Also indicate progress in the files view - progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => downloadPromise); }; CommandsRegistry.registerCommand({ @@ -1303,24 +1300,24 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { return { source: fileToPaste, target: targetFile }; })); - // Move/Copy File - if (pasteShouldMove) { - const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); - const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('movingBulkEdit', "Moving {0} files", sourceTargetPairs.length) : nls.localize('movingFileBulkEdit', "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('moveBulkEdit', "Move {0} files", sourceTargetPairs.length) : nls.localize('moveFileBulkEdit', "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) - }; - await explorerService.applyBulkEdit(resourceFileEdits, options); - } else { - const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); - const options = { - progressLabel: sourceTargetPairs.length > 1 ? nls.localize('copyingBulkEdit', "Copying {0} files", sourceTargetPairs.length) : nls.localize('copyingFileBulkEdit', "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), - undoLabel: sourceTargetPairs.length > 1 ? nls.localize('copyBulkEdit', "Copy {0} files", sourceTargetPairs.length) : nls.localize('copyFileBulkEdit', "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) - }; - await explorerService.applyBulkEdit(resourceFileEdits, options); - } - if (sourceTargetPairs.length >= 1) { + // Move/Copy File + if (pasteShouldMove) { + const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target)); + const options = { + progressLabel: sourceTargetPairs.length > 1 ? nls.localize('movingBulkEdit', "Moving {0} files", sourceTargetPairs.length) : nls.localize('movingFileBulkEdit', "Moving {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize('moveBulkEdit', "Move {0} files", sourceTargetPairs.length) : nls.localize('moveFileBulkEdit', "Move {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + }; + await explorerService.applyBulkEdit(resourceFileEdits, options); + } else { + const resourceFileEdits = sourceTargetPairs.map(pair => new ResourceFileEdit(pair.source, pair.target, { copy: true })); + const options = { + progressLabel: sourceTargetPairs.length > 1 ? nls.localize('copyingBulkEdit', "Copying {0} files", sourceTargetPairs.length) : nls.localize('copyingFileBulkEdit', "Copying {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)), + undoLabel: sourceTargetPairs.length > 1 ? nls.localize('copyBulkEdit', "Copy {0} files", sourceTargetPairs.length) : nls.localize('copyFileBulkEdit', "Copy {0}", resources.basenameOrAuthority(sourceTargetPairs[0].target)) + }; + await explorerService.applyBulkEdit(resourceFileEdits, options); + } + const pair = sourceTargetPairs[0]; await explorerService.select(pair.target); if (sourceTargetPairs.length === 1) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 71b44a81d1d..9e8330b3dd8 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -20,7 +20,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; -import { dirname, joinPath, basename, distinctParents, basenameOrAuthority } from 'vs/base/common/resources'; +import { dirname, joinPath, basename, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -982,7 +982,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void { + async drop(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise { this.compressedDropTargetDisposable.dispose(); // Find compressed target @@ -1013,24 +1013,28 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (data instanceof NativeDragAndDropData) { const cts = new CancellationTokenSource(); - // Indicate progress globally - try { - if (isWeb) { - const dropPromise = this.progressService.withProgress({ - location: ProgressLocation.Window, - delay: 800, - cancellable: true, - title: localize('uploadingFiles', "Uploading") - }, async progress => { - this.handleWebExternalDrop(resolvedTarget, originalEvent, progress, cts.token); - }, () => cts.dispose(true)); - // Also indicate progress in the files view - this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => dropPromise); - } else { - this.handleExternalDrop(resolvedTarget, originalEvent, cts.token); + if (isWeb) { + // Indicate progress globally + const dropPromise = this.progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('uploadingFiles', "Uploading") + }, async progress => { + try { + await this.handleWebExternalDrop(resolvedTarget, originalEvent, progress, cts.token); + } catch (error) { + this.notificationService.warn(error); + } + }, () => cts.dispose(true)); + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 500 }, () => dropPromise); + } else { + try { + await this.handleExternalDrop(resolvedTarget, originalEvent, cts.token); + } catch (error) { + this.notificationService.warn(error); } - } catch (error) { - this.notificationService.warn(error); } } // In-Explorer DND (Move/Copy file) @@ -1074,7 +1078,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } await this.explorerService.applyBulkEdit([new ResourceFileEdit(joinPath(target.resource, entry.name), undefined, { recursive: true })], { - undoLabel: localize('overwrite', "Overwriting {0}", entry.name), + undoLabel: localize('overwrite', "Overwrite {0}", entry.name), progressLabel: localize('overwriting', "Overwriting {0}", entry.name), }); @@ -1345,14 +1349,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); await this.explorerService.applyBulkEdit(resourceFileEdits, { - undoLabel: resourcesFiltered.length === 1 ? localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : localize('copynFile', "Copy {0} files", resourcesFiltered.length), - progressLabel: resourcesFiltered.length === 1 ? localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : localize('copyingnFile', "Copying {0} files", resourcesFiltered.length) + undoLabel: resourcesFiltered.length === 1 ? localize('copyFile', "Copy {0}", basename(resourcesFiltered[0])) : localize('copynFile', "Copy {0} resources", resourcesFiltered.length), + progressLabel: resourcesFiltered.length === 1 ? localize('copyingFile', "Copying {0}", basename(resourcesFiltered[0])) : localize('copyingnFile', "Copying {0} resources", resourcesFiltered.length) }); // if we only add one file, just open it directly if (resourceFileEdits.length === 1) { const item = this.explorerService.findClosest(resourceFileEdits[0].newResource!); - if (item) { + if (item && !item.isDirectory) { this.editorService.openEditor({ resource: item.resource, options: { pinned: true } }); } } @@ -1440,9 +1444,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Reuse duplicate action when user copies const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; const resourceFileEdits = sources.map(({ resource, isDirectory }) => (new ResourceFileEdit(resource, findValidPasteFileTarget(this.explorerService, target, { resource, isDirectory, allowOverwrite: false }, incrementalNaming), { copy: true }))); + const labelSufix = getFileOrFolderLabelSufix(sources); await this.explorerService.applyBulkEdit(resourceFileEdits, { - undoLabel: resourceFileEdits.length > 1 ? localize('copy', "Copy {0} files", resourceFileEdits.length) : localize('copyOneFile', "Copy {0}", basenameOrAuthority(resourceFileEdits[0].newResource!)), - progressLabel: resourceFileEdits.length > 1 ? localize('copying', "Copying {0} files", resourceFileEdits.length) : localize('copyingOneFile', "Copying {0}", basenameOrAuthority(resourceFileEdits[0].newResource!)), + undoLabel: localize('copy', "Copy {0}", labelSufix), + progressLabel: localize('copying', "Copying {0}", labelSufix), }); const editors = resourceFileEdits.filter(edit => { @@ -1457,9 +1462,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Do not allow moving readonly items const resourceFileEdits = sources.filter(source => !source.isReadonly).map(source => new ResourceFileEdit(source.resource, joinPath(target.resource, source.name))); + const labelSufix = getFileOrFolderLabelSufix(sources); const options = { - undoLabel: sources.length > 1 ? localize('move', "Move {0} files", sources.length) : localize('moveOneFile', "Move {0}", sources[0].name), - progressLabel: sources.length > 1 ? localize('moving', "Moving {0} files", sources.length) : localize('movingOneFile', "Moving {0}", sources[0].name), + undoLabel: localize('move', "Move {0}", labelSufix), + progressLabel: localize('moving', "Moving {0}", labelSufix) }; try { @@ -1564,3 +1570,18 @@ export class ExplorerCompressionDelegate implements ITreeCompressionDelegate i.isDirectory)) { + return `${items.length} folders`; + } + if (items.every(i => !i.isDirectory)) { + return `${items.length} files`; + } + + return `${items.length} files and folders`; +} diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 8c2a78e037d..3535108c329 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -267,20 +267,16 @@ export class OpenEditorsView extends ViewPane { })); const resourceNavigator = this._register(new ListResourceNavigator(this.list, { configurationService: this.configurationService })); this._register(resourceNavigator.onDidOpen(e => { - if (typeof e.element !== 'number') { + if (!e.element) { return; - } - - const element = this.list.element(e.element); - - if (element instanceof OpenEditor) { + } else if (e.element instanceof OpenEditor) { if (e.browserEvent instanceof MouseEvent && e.browserEvent.button === 1) { return; // middle click already handled above: closes the editor } - this.openEditor(element, { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, sideBySide: e.sideBySide }); + this.openEditor(e.element, { preserveFocus: e.editorOptions.preserveFocus, pinned: e.editorOptions.pinned, sideBySide: e.sideBySide }); } else { - this.editorGroupService.activateGroup(element); + this.editorGroupService.activateGroup(e.element); } })); diff --git a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 8fdd7c5b600..72211dd04ca 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -14,6 +14,8 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-sandbox/fileCommands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; @@ -21,7 +23,6 @@ import { ResourceContextKey } from 'vs/workbench/common/resources'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; import { SideBySideEditor, EditorResourceAccessor } from 'vs/workbench/common/editor'; import { ContextKeyOrExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; const REVEAL_IN_OS_COMMAND_ID = 'revealFileInOS'; const REVEAL_IN_OS_LABEL = isWindows ? nls.localize('revealInWindows', "Reveal in File Explorer") : isMacintosh ? nls.localize('revealInMac', "Reveal in Finder") : nls.localize('openContainer', "Open Containing Folder"); @@ -35,9 +36,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ win: { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_R }, - handler: (accessor: ServicesAccessor, _resource: URI | object) => { - const explorerService = accessor.get(IExplorerService); - const resources = explorerService.getContext(false).map(item => item.resource); + handler: (accessor: ServicesAccessor, resource: URI | object) => { + const resources = getMultiSelectedResources(resource, accessor.get(IListService), accessor.get(IEditorService), accessor.get(IExplorerService)); revealResourcesInOS(resources, accessor.get(INativeHostService), accessor.get(INotificationService), accessor.get(IWorkspaceContextService)); } }); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 8e7ff32342c..8836c8371b2 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -54,6 +54,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { groupBy } from 'vs/base/common/arrays'; import { ResourceMap } from 'vs/base/common/map'; +import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterable> { return Iterable.map(resourceMarkers.markers, m => { @@ -327,11 +328,15 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } private setTreeSelection(): void { - if (this.tree && this.tree.getSelection().length === 0) { - const firstMarker = this.markersModel.resourceMarkers[0]?.markers[0]; - if (firstMarker) { - this.tree.setFocus([firstMarker]); - this.tree.setSelection([firstMarker]); + if (this.tree && this.tree.isVisible() && this.tree.getSelection().length === 0) { + const firstVisibleElement = this.tree.firstVisibleElement; + const marker = firstVisibleElement ? + firstVisibleElement instanceof ResourceMarkers ? firstVisibleElement.markers[0] : + firstVisibleElement instanceof Marker ? firstVisibleElement : undefined + : undefined; + if (marker) { + this.tree.setFocus([marker]); + this.tree.setSelection([marker]); } } } @@ -603,7 +608,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private setCurrentActiveEditor(): void { const activeEditor = this.editorService.activeEditor; - this.currentActiveResource = activeEditor ? withUndefinedAsNull(activeEditor.resource) : null; + this.currentActiveResource = activeEditor ? withUndefinedAsNull(EditorResourceAccessor.getOriginalUri(activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY })) : null; } private onSelected(): void { @@ -946,6 +951,10 @@ class MarkersTree extends WorkbenchObjectTree { this.container.classList.toggle('hidden', hide); } + isVisible(): boolean { + return !this.container.classList.contains('hidden'); + } + } registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts index fdabea9f6f1..2c2c46b895a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findController.ts @@ -43,6 +43,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote private _currentMatchDecorations: ICellModelDecorations[] = []; private _showTimeout: number | null = null; private _hideTimeout: number | null = null; + private _previousFocusElement?: HTMLElement; constructor( private readonly _notebookEditor: INotebookEditor, @@ -65,6 +66,10 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._register(this._state.onFindReplaceStateChange(() => { this.onInputChanged(); })); + + this._register(DOM.addDisposableListener(this.getDomNode(), DOM.EventType.FOCUS, e => { + this._previousFocusElement = e.relatedTarget instanceof HTMLElement ? e.relatedTarget : undefined; + }, true)); } private _onFindInputKeyDown(e: IKeyboardEvent): void { @@ -167,6 +172,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } protected onFocusTrackerBlur() { + this._previousFocusElement = undefined; this._findWidgetFocused.reset(); } @@ -324,6 +330,11 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote } else { // no op } + + if (this._previousFocusElement && this._previousFocusElement.offsetParent) { + this._previousFocusElement.focus(); + this._previousFocusElement = undefined; + } } clear() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts deleted file mode 100644 index 670a805cf79..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts +++ /dev/null @@ -1,163 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser'; -import { registerNotebookContribution } from '../../notebookEditorExtensions'; -import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; -import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; -import { first, ThrottledDelayer } from 'vs/base/common/async'; -import { INotebookService } from '../../../common/notebookService'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; - -export class SCMController extends Disposable implements INotebookEditorContribution { - static id: string = 'workbench.notebook.findController'; - private _lastDecorationId: string[] = []; - private _localDisposable = new DisposableStore(); - private _originalDocument: NotebookTextModel | undefined = undefined; - private _originalResourceDisposableStore = new DisposableStore(); - private _diffDelayer = new ThrottledDelayer(200); - - private _lastVersion = -1; - - - constructor( - private readonly _notebookEditor: INotebookEditor, - @IFileService private readonly _fileService: FileService, - @ISCMService private readonly _scmService: ISCMService, - @INotebookService private readonly _notebookService: INotebookService - - ) { - super(); - - if (!this._notebookEditor.isEmbedded) { - this._register(this._notebookEditor.onDidChangeModel(() => { - this._localDisposable.clear(); - this._originalResourceDisposableStore.clear(); - this._diffDelayer.cancel(); - this.update(); - - if (this._notebookEditor.textModel) { - this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent((e) => { - this.update(); - })); - } - })); - - this._register(this._notebookEditor.onWillDispose(() => { - this._localDisposable.clear(); - this._originalResourceDisposableStore.clear(); - })); - - this.update(); - } - } - - private async _resolveNotebookDocument(uri: URI, viewType: string) { - const providers = this._scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); - - rootedProviders.sort(createProviderComparer(uri)); - - const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); - - if (!result) { - this._originalDocument = undefined; - this._originalResourceDisposableStore.clear(); - return; - } - - if (result.toString() === this._originalDocument?.uri.toString()) { - // original document not changed - return; - } - - this._originalResourceDisposableStore.add(this._fileService.watch(result)); - this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => { - if (e.contains(result)) { - this._originalDocument = undefined; - this._originalResourceDisposableStore.clear(); - this.update(); - } - })); - - const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false); - this._originalResourceDisposableStore.add({ - dispose: () => { - this._originalDocument?.dispose(); - this._originalDocument = undefined; - } - }); - - this._originalDocument = originalDocument; - } - - async update() { - if (!this._diffDelayer) { - return; - } - - await this._diffDelayer - .trigger(async () => { - const modifiedDocument = this._notebookEditor.textModel; - if (!modifiedDocument) { - return; - } - - if (this._lastVersion >= modifiedDocument.versionId) { - return; - } - - this._lastVersion = modifiedDocument.versionId; - await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType); - - if (!this._originalDocument) { - this._clear(); - return; - } - - // const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument)); - // const diffResult = diff.ComputeDiff(false); - - // const decorations: INotebookDeltaDecoration[] = []; - // diffResult.changes.forEach(change => { - // if (change.originalLength === 0) { - // // doesn't exist in original - // for (let i = 0; i < change.modifiedLength; i++) { - // decorations.push({ - // handle: modifiedDocument.cells[change.modifiedStart + i].handle, - // options: { gutterClassName: 'nb-gutter-cell-inserted' } - // }); - // } - // } else { - // if (change.modifiedLength === 0) { - // // diff.deleteCount - // // removed from original - // } else { - // // modification - // for (let i = 0; i < change.modifiedLength; i++) { - // decorations.push({ - // handle: modifiedDocument.cells[change.modifiedStart + i].handle, - // options: { gutterClassName: 'nb-gutter-cell-changed' } - // }); - // } - // } - // } - // }); - - - // this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations); - }); - } - - private _clear() { - this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []); - } -} - -registerNotebookContribution(SCMController.id, SCMController); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 11a3fd0ebdf..c2b2830c4c9 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -535,6 +535,11 @@ cursor: grab; } +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { + z-index: 25; + cursor: default; +} + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator .codicon:hover { cursor: pointer; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index c0d6564bdad..6780f1fa93f 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -29,4 +29,4 @@ export const collapsedIcon = registerIcon('notebook-collapsed', Codicon.chevronR export const expandedIcon = registerIcon('notebook-expanded', Codicon.chevronDown, localize('expandedIcon', 'Icon to annotated a expanded section in notebook editors.')); export const openAsTextIcon = registerIcon('notebook-open-as-text', Codicon.fileCode, localize('openAsTextIcon', 'Icon to open the notebook in a text editor.')); export const revertIcon = registerIcon('notebook-revert', Codicon.discard, localize('revertIcon', 'Icon to revert in notebook editors.')); -export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type notebook editors.')); +export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts index 8ef36f85029..1f9ccd05aa4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/textHelper.ts @@ -88,7 +88,7 @@ export function truncatedArrayOfString(container: HTMLElement, outputs: string[] if (buffer.getLineCount() < LINES_LIMIT) { const lineCount = buffer.getLineCount(); - const fullRange = new Range(1, 1, lineCount, buffer.getLineLastNonWhitespaceColumn(lineCount)); + const fullRange = new Range(1, 1, lineCount, Math.max(1, buffer.getLineLastNonWhitespaceColumn(lineCount))); container.innerText = buffer.getValueInRange(fullRange, EndOfLinePreference.TextDefined); return; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index ecc1655a00f..2fccd56c7b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -1034,6 +1034,7 @@ export class RunStateRenderer { clear() { if (this.spinnerTimer) { clearTimeout(this.spinnerTimer); + this.spinnerTimer = undefined; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts index 5560bbeb119..6be1604eea1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -12,6 +12,7 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; import { extUri } from 'vs/base/common/resources'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { IDimension } from 'vs/editor/common/editorCommon'; @@ -352,8 +353,8 @@ export function getExecuteCellPlaceholder(viewCell: BaseCellViewModel) { command: undefined, // text: `${keybinding?.getLabel() || 'Ctrl + Enter'} to run`, // tooltip: `${keybinding?.getLabel() || 'Ctrl + Enter'} to run`, - text: 'Ctrl + Enter to run', - tooltip: 'Ctrl + Enter to run', + text: isWindows ? 'Ctrl + Alt + Enter' : 'Ctrl + Enter to run', + tooltip: isWindows ? 'Ctrl + Alt + Enter' : 'Ctrl + Enter to run', visible: true, opacity: '0.7' }; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index fd79380d7c9..1ad9b60a8b6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -17,6 +17,8 @@ import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService } from 'vs/platform/files/common/files'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { TaskSequentializer } from 'vs/base/common/async'; export interface INotebookLoadOptions { @@ -40,6 +42,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM private readonly _name: string; private readonly _workingCopyResource: URI; + private readonly saveSequentializer = new TaskSequentializer(); private _dirty = false; @@ -51,6 +54,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM @IBackupFileService private readonly _backupFileService: IBackupFileService, @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, + @ILogService private readonly _logService: ILogService, @ILabelService labelService: ILabelService, ) { super(); @@ -197,8 +201,14 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM } private async _assertStat() { + this._logService.debug('[notebook editor model] start assert stat'); const stats = await this._resolveStats(this.resource); if (this._lastResolvedFileStat && stats && stats.mtime > this._lastResolvedFileStat.mtime) { + this._logService.debug(`[notebook editor model] noteboook file on disk is newer: +LastResolvedStat: ${this._lastResolvedFileStat ? JSON.stringify(this._lastResolvedFileStat) : undefined}. +Current stat: ${JSON.stringify(stats)} +`); + this._lastResolvedFileStat = stats; return new Promise<'overwrite' | 'revert' | 'none'>(resolve => { const handle = this._notificationService.prompt( Severity.Info, @@ -221,31 +231,57 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM resolve('none'); }); }); + } else if (!this._lastResolvedFileStat && stats) { + // finally get a stats + this._lastResolvedFileStat = stats; } return 'overwrite'; } async save(): Promise { - const result = await this._assertStat(); - if (result === 'none') { - return false; + let versionId = this._notebook.versionId; + this._logService.debug(`[notebook editor model] save(${versionId}) - enter with versionId ${versionId}`, this.resource.toString(true)); + + if (this.saveSequentializer.hasPending(versionId)) { + this._logService.debug(`[notebook editor model] save(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString(true)); + return this.saveSequentializer.pending.then(() => { + return true; + }); } - if (result === 'revert') { - await this.revert(); + if (this.saveSequentializer.hasPending()) { + return this.saveSequentializer.setNext(async () => { + await this.save(); + }).then(() => { + return true; + }); + } + + return this.saveSequentializer.setPending(versionId, (async () => { + const result = await this._assertStat(); + if (result === 'none') { + return; + } + + if (result === 'revert') { + await this.revert(); + return; + } + + const tokenSource = new CancellationTokenSource(); + await this._notebookService.save(this.notebook.viewType, this.notebook.uri, tokenSource.token); + this._logService.debug(`[notebook editor model] save(${versionId}) - document saved saved, start updating file stats`, this.resource.toString(true)); + const newStats = await this._resolveStats(this.resource); + this._lastResolvedFileStat = newStats; + this.setDirty(false); + })()).then(() => { return true; - } - - const tokenSource = new CancellationTokenSource(); - await this._notebookService.save(this.notebook.viewType, this.notebook.uri, tokenSource.token); - const newStats = await this._resolveStats(this.resource); - this._lastResolvedFileStat = newStats; - this.setDirty(false); - return true; + }); } async saveAs(targetResource: URI): Promise { + this._logService.debug(`[notebook editor model] saveAs - enter`, this.resource.toString(true)); const result = await this._assertStat(); if (result === 'none') { @@ -259,6 +295,7 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM const tokenSource = new CancellationTokenSource(); await this._notebookService.saveAs(this.notebook.viewType, this.notebook.uri, targetResource, tokenSource.token); + this._logService.debug(`[notebook editor model] saveAs - document saved, start updating file stats`, this.resource.toString(true)); const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; this.setDirty(false); @@ -271,7 +308,9 @@ export class NotebookEditorModel extends EditorModel implements INotebookEditorM } try { + this._logService.debug(`[notebook editor model] _resolveStats`, this.resource.toString(true)); const newStats = await this._fileService.resolve(this.resource, { resolveMetadata: true }); + this._logService.debug(`[notebook editor model] _resolveStats - latest file stats: ${JSON.stringify(newStats)}`, this.resource.toString(true)); return newStats; } catch (e) { return undefined; diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index ab48b16b7ca..3062e937dce 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -149,10 +149,9 @@ class SimpleToggleAction extends Action { private readonly _listener: IDisposable; constructor(state: OutlineViewState, label: string, isChecked: () => boolean, callback: (action: SimpleToggleAction) => any, className?: string) { - super(`simple` + defaultGenerator.nextId(), label, className, true, () => { + super(`simple` + defaultGenerator.nextId(), label, className, true, async () => { this.checked = !this.checked; callback(this); - return Promise.resolve(); }); this.checked = isChecked(); this._listener = state.onDidChange(() => this.checked = isChecked()); diff --git a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts index 6569ec09cff..60f7caa47e1 100644 --- a/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts +++ b/src/vs/workbench/contrib/performance/browser/perfviewEditor.ts @@ -108,7 +108,7 @@ class PerfModelContentProvider implements ITextModelContentProvider { this._modelDisposables.push(langId); this._modelDisposables.push(this._extensionService.onDidChangeExtensionsStatus(this._updateModel, this)); - writeTransientState(this._model, { forceWordWrap: 'off', forceWordWrapMinified: false }, this._editorService); + writeTransientState(this._model, { wordWrapOverride: 'off' }, this._editorService); } this._updateModel(); return Promise.resolve(this._model); diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 43c3806aa16..7b7c3dd59ed 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -691,8 +691,10 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP ...(keybindingItemEntry.keybindingItem.keybinding ? [this.createDefineKeybindingAction(keybindingItemEntry), this.createAddKeybindingAction(keybindingItemEntry)] : [this.createDefineKeybindingAction(keybindingItemEntry)]), + new Separator(), this.createRemoveAction(keybindingItemEntry), this.createResetAction(keybindingItemEntry), + new Separator(), this.createDefineWhenExpressionAction(keybindingItemEntry), new Separator(), this.createShowConflictsAction(keybindingItemEntry)] @@ -713,7 +715,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP private createDefineKeybindingAction(keybindingItemEntry: IKeybindingItemEntry): IAction { return { - label: keybindingItemEntry.keybindingItem.keybinding ? localize('changeLabel', "Change Keybinding") : localize('addLabel', "Add Keybinding"), + label: keybindingItemEntry.keybindingItem.keybinding ? localize('changeLabel', "Change Keybinding...") : localize('addLabel', "Add Keybinding..."), enabled: true, id: KEYBINDINGS_EDITOR_COMMAND_DEFINE, run: () => this.defineKeybinding(keybindingItemEntry, false) @@ -722,7 +724,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP private createAddKeybindingAction(keybindingItemEntry: IKeybindingItemEntry): IAction { return { - label: localize('addLabel', "Add Keybinding"), + label: localize('addLabel', "Add Keybinding..."), enabled: true, id: KEYBINDINGS_EDITOR_COMMAND_ADD, run: () => this.defineKeybinding(keybindingItemEntry, true) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index d6c34ed2e81..7abb3fca76f 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -735,7 +735,7 @@ export class SettingsEditor2 extends EditorPane { private notifyNoSaveNeeded() { if (!this.storageService.getBoolean(SETTINGS_AUTOSAVE_NOTIFIED_KEY, StorageScope.GLOBAL, false)) { this.storageService.store(SETTINGS_AUTOSAVE_NOTIFIED_KEY, true, StorageScope.GLOBAL, StorageTarget.USER); - this.notificationService.info(localize('settingsNoSaveNeeded', "Your changes are automatically saved as you edit.")); + this.notificationService.info(localize('settingsNoSaveNeeded', "Changes to settings are saved automatically.")); } } diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index c5a10561fec..9f8bede88ca 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -36,7 +36,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private updateMode: string | undefined; private debugConsoleWordWrap: boolean | undefined; private accessibilitySupport: 'on' | 'off' | 'auto' | undefined; - private enableExperimentalProxyLoginDialog = true; // default constructor( @IHostService private readonly hostService: IHostService, @@ -98,12 +97,6 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo changed = true; } } - - // Experimental proxy login dialog - if (typeof config.window?.enableExperimentalProxyLoginDialog === 'boolean' && config.window.enableExperimentalProxyLoginDialog !== this.enableExperimentalProxyLoginDialog) { - this.enableExperimentalProxyLoginDialog = config.window.enableExperimentalProxyLoginDialog; - changed = true; - } } // Notify only when changed and we are the focused window (avoids notification spam across windows) diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 2df1d1cfaf2..29ab926a392 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -574,7 +574,7 @@ Registry.as(Extensions.ViewContainersRegistry).register matches = /^details(@(\d+))?$/.exec(group); if (matches) { - return -500; + return -500 + Number(matches[2]); } matches = /^help(@(\d+))?$/.exec(group); diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 138dd8ed5d2..3813ddcc697 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -213,21 +213,48 @@ class ForwardedPortNotifier extends Disposable { this.lastNotifyTime.setFullYear(this.lastNotifyTime.getFullYear() - 1); } - public notify(tunnels: RemoteTunnel[]) { - if (Date.now() - this.lastNotifyTime.getTime() > ForwardedPortNotifier.COOL_DOWN) { - this.showNotification(tunnels); + public async notify(tunnels: RemoteTunnel[]) { + const tunnel = await this.portNumberHeuristicDelay(tunnels); + if (tunnel) { + if (Date.now() - this.lastNotifyTime.getTime() > ForwardedPortNotifier.COOL_DOWN) { + this.showNotification(tunnel); + } } } - private showNotification(tunnels: RemoteTunnel[]) { + private newerTunnel: RemoteTunnel | undefined; + private async portNumberHeuristicDelay(tunnels: RemoteTunnel[]): Promise { if (tunnels.length === 0) { return; } tunnels = tunnels.sort((a, b) => a.tunnelRemotePort - b.tunnelRemotePort); const firstTunnel = tunnels.shift()!; - const address = makeAddress(firstTunnel.tunnelRemoteHost, firstTunnel.tunnelRemotePort); + // Heuristic. + if (firstTunnel.tunnelRemotePort % 1000 === 0) { + this.newerTunnel = firstTunnel; + return firstTunnel; + // 9229 is the node inspect port + } else if (firstTunnel.tunnelRemotePort < 10000 && firstTunnel.tunnelRemotePort !== 9229) { + this.newerTunnel = firstTunnel; + return firstTunnel; + } + + this.newerTunnel = undefined; + return new Promise(resolve => { + setTimeout(() => { + if (this.newerTunnel) { + resolve(undefined); + } else { + resolve(firstTunnel); + } + }, 3000); + }); + } + + private showNotification(tunnel: RemoteTunnel) { + const address = makeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); const message = nls.localize('remote.tunnelsView.automaticForward', "Your service running on port {0} is available. [See all forwarded ports](command:{1}.focus)", - firstTunnel.tunnelRemotePort, TunnelPanel.ID); + tunnel.tunnelRemotePort, TunnelPanel.ID); const browserChoice: IPromptChoice = { label: OpenPortInBrowserAction.LABEL, run: () => OpenPortInBrowserAction.run(this.remoteExplorerService.tunnelModel, this.openerService, address) @@ -321,24 +348,24 @@ class LinuxAutomaticPortForwarding extends Disposable { ) { super(); this.notifier = new ForwardedPortNotifier(notificationService, remoteExplorerService, openerService); - this._register(configurationService.onDidChangeConfiguration((e) => { + this._register(configurationService.onDidChangeConfiguration(async (e) => { if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) { - this.startStopCandidateListener(); + await this.startStopCandidateListener(); } })); - this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(e => { + this.contextServiceListener = this._register(this.contextKeyService.onDidChangeContext(async (e) => { if (e.affectsSome(new Set(forwardedPortsViewEnabled.keys()))) { - this.startStopCandidateListener(); + await this.startStopCandidateListener(); } })); this.startStopCandidateListener(); } - private startStopCandidateListener() { + private async startStopCandidateListener() { if (this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)) { - this.startCandidateListener(); + await this.startCandidateListener(); } else { this.stopCandidateListener(); } @@ -351,7 +378,7 @@ class LinuxAutomaticPortForwarding extends Disposable { } } - private startCandidateListener() { + private async startCandidateListener() { if (this.candidateListener || !forwardedPortsViewEnabled.getValue(this.contextKeyService)) { return; } @@ -359,9 +386,14 @@ class LinuxAutomaticPortForwarding extends Disposable { this.contextServiceListener.dispose(); } - this.candidateListener = this._register(this.remoteExplorerService.tunnelModel.onCandidatesChanged(this.handleCandidateUpdate, this)); + if (!this.remoteExplorerService.tunnelModel.environmentTunnelsSet) { + await new Promise(resolve => this.remoteExplorerService.tunnelModel.onEnvironmentTunnelsSet(() => resolve())); + } + // Capture list of starting candidates so we don't auto forward them later. this.setInitialCandidates(); + + this.candidateListener = this._register(this.remoteExplorerService.tunnelModel.onCandidatesChanged(this.handleCandidateUpdate, this)); } private setInitialCandidates() { @@ -376,6 +408,9 @@ class LinuxAutomaticPortForwarding extends Disposable { if (this.initialCandidates.has(address)) { return undefined; } + if (mapHasAddressLocalhostOrAllInterfaces(this.remoteExplorerService.tunnelModel.detected, value.host, value.port)) { + return undefined; + } const forwarded = await this.remoteExplorerService.forward(value); if (forwarded) { this.autoForwarded.add(address); diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index e27ba8dc03f..7ed9fa790e0 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -292,6 +292,9 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer): Promise { + private async open(e: IOpenEvent): Promise { if (!e.element) { return; } else if (isSCMRepository(e.element)) { diff --git a/src/vs/workbench/contrib/search/browser/searchIcons.ts b/src/vs/workbench/contrib/search/browser/searchIcons.ts index e556aa76df5..8c6d633bde0 100644 --- a/src/vs/workbench/contrib/search/browser/searchIcons.ts +++ b/src/vs/workbench/contrib/search/browser/searchIcons.ts @@ -9,12 +9,12 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; export const searchDetailsIcon = registerIcon('search-details', Codicon.ellipsis, localize('searchDetailsIcon', 'Icon to make search details visible.')); -export const searchShowContextIcon = registerIcon('search-show-context', Codicon.listSelection, localize('searchShowContextIcon', 'Icon for toggle the context in the search editor')); +export const searchShowContextIcon = registerIcon('search-show-context', Codicon.listSelection, localize('searchShowContextIcon', 'Icon for toggle the context in the search editor.')); export const searchHideReplaceIcon = registerIcon('search-hide-replace', Codicon.chevronRight, localize('searchHideReplaceIcon', 'Icon to collapse the replace section in the search view.')); export const searchShowReplaceIcon = registerIcon('search-show-replace', Codicon.chevronDown, localize('searchShowReplaceIcon', 'Icon to expand the replace section in the search view.')); export const searchReplaceAllIcon = registerIcon('search-replace-all', Codicon.replaceAll, localize('searchReplaceAllIcon', 'Icon for replace all in the search view.')); export const searchReplaceIcon = registerIcon('search-replace', Codicon.replace, localize('searchReplaceIcon', 'Icon for replace in the search view.')); -export const searchRemoveIcon = registerIcon('search-remove', Codicon.close, localize('searchRemoveIcon', 'Icon to remove a search result')); +export const searchRemoveIcon = registerIcon('search-remove', Codicon.close, localize('searchRemoveIcon', 'Icon to remove a search result.')); export const searchRefreshIcon = registerIcon('search-refresh', Codicon.refresh, localize('searchRefreshIcon', 'Icon for refresh in the search view.')); export const searchCollapseAllIcon = registerIcon('search-collapse-results', Codicon.collapseAll, localize('searchCollapseAllIcon', 'Icon for collapse results in the search view.')); diff --git a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts index 9bd262f90b5..0271760eaf9 100644 --- a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts @@ -18,6 +18,7 @@ import { TestPathService, TestEnvironmentService } from 'vs/workbench/test/brows import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; +import { extUriBiasedIgnorePathCase } from 'vs/base/common/resources'; const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true, useGlobalIgnoreFiles: true }; @@ -287,7 +288,7 @@ suite('QueryBuilder', () => { const ROOT_2_URI = getUri(ROOT_2); const ROOT_3 = fixPath('/project/root3'); const ROOT_3_URI = getUri(ROOT_3); - mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: ROOT_2_URI.fsPath }, { path: ROOT_3_URI.fsPath }], WS_CONFIG_PATH); + mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: ROOT_2_URI.fsPath }, { path: ROOT_3_URI.fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('/config')); mockConfigService.setUserConfiguration('search', { @@ -673,7 +674,7 @@ suite('QueryBuilder', () => { test('relative includes w/two root folders', () => { const ROOT_2 = '/project/root2'; - mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH); + mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('config')); const cases: [string, ISearchPathsInfo][] = [ @@ -714,7 +715,7 @@ suite('QueryBuilder', () => { test('include ./foldername', () => { const ROOT_2 = '/project/root2'; const ROOT_1_FOLDERNAME = 'foldername'; - mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath, name: ROOT_1_FOLDERNAME }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH); + mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath, name: ROOT_1_FOLDERNAME }, { path: getUri(ROOT_2).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('config')); const cases: [string, ISearchPathsInfo][] = [ @@ -742,7 +743,7 @@ suite('QueryBuilder', () => { test('relative includes w/multiple ambiguous root folders', () => { const ROOT_2 = '/project/rootB'; const ROOT_3 = '/otherproject/rootB'; - mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }, { path: getUri(ROOT_3).fsPath }], WS_CONFIG_PATH); + mockWorkspace.folders = toWorkspaceFolders([{ path: ROOT_1_URI.fsPath }, { path: getUri(ROOT_2).fsPath }, { path: getUri(ROOT_3).fsPath }], WS_CONFIG_PATH, extUriBiasedIgnorePathCase); mockWorkspace.configuration = uri.file(fixPath('/config')); const cases: [string, ISearchPathsInfo][] = [ diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index ebcd6ca4eb1..cc6faa97d74 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -234,7 +234,6 @@ registerAction2(class extends Action2 { title: { value: localize('searchEditor.deleteResultBlock', "Delete File Results"), original: 'Delete File Results' }, keybinding: { weight: KeybindingWeight.EditorContrib, - when: SearchEditorConstants.InSearchEditor, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace, }, precondition: SearchEditorConstants.InSearchEditor, @@ -361,13 +360,10 @@ registerAction2(class extends Action2 { id: FocusQueryEditorWidgetCommandId, title: { value: localize('search.action.focusQueryEditorWidget', "Focus Search Editor Input"), original: 'Focus Search Editor Input' }, category, - menu: { - id: MenuId.CommandPalette, - when: ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID) - }, + f1: true, + precondition: SearchEditorConstants.InSearchEditor, keybinding: { primary: KeyCode.Escape, - when: SearchEditorConstants.InSearchEditor, weight: KeybindingWeight.EditorContrib } }); @@ -388,10 +384,10 @@ registerAction2(class extends Action2 { title: { value: localize('searchEditor.action.toggleSearchEditorCaseSensitive', "Toggle Match Case"), original: 'Toggle Match Case' }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: Object.assign({ weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), + when: SearchConstants.SearchInputBoxFocusedKey, }, ToggleCaseSensitiveKeybinding) }); } @@ -407,10 +403,10 @@ registerAction2(class extends Action2 { title: { value: localize('searchEditor.action.toggleSearchEditorWholeWord', "Toggle Match Whole Word"), original: 'Toggle Match Whole Word' }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: Object.assign({ weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), + when: SearchConstants.SearchInputBoxFocusedKey, }, ToggleWholeWordKeybinding) }); } @@ -426,10 +422,10 @@ registerAction2(class extends Action2 { title: { value: localize('searchEditor.action.toggleSearchEditorRegex', "Toggle Use Regular Expression"), original: 'Toggle Use Regular Expression"' }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: Object.assign({ weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, SearchConstants.SearchInputBoxFocusedKey), + when: SearchConstants.SearchInputBoxFocusedKey, }, ToggleRegexKeybinding) }); } @@ -445,10 +441,9 @@ registerAction2(class extends Action2 { title: { value: localize('searchEditor.action.toggleSearchEditorContextLines', "Toggle Context Lines"), original: 'Toggle Context Lines"' }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), primary: KeyMod.Alt | KeyCode.KEY_L, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } } @@ -466,10 +461,9 @@ registerAction2(class extends Action2 { title: { original: 'Increase Context Lines', value: localize('searchEditor.action.increaseSearchEditorContextLines', "Increase Context Lines") }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), primary: KeyMod.Alt | KeyCode.US_EQUAL } }); @@ -484,10 +478,9 @@ registerAction2(class extends Action2 { title: { original: 'Decrease Context Lines', value: localize('searchEditor.action.decreaseSearchEditorContextLines', "Decrease Context Lines") }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), primary: KeyMod.Alt | KeyCode.US_MINUS } }); @@ -502,10 +495,9 @@ registerAction2(class extends Action2 { title: { original: 'Select All Matches', value: localize('searchEditor.action.selectAllSearchEditorMatches', "Select All Matches") }, category, f1: true, - precondition: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + precondition: SearchEditorConstants.InSearchEditor, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L, } }); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index fb420ef52ae..c2910ec1192 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -9,7 +9,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { TerminalLink, OPEN_FILE_LABEL, FOLDER_IN_WORKSPACE_LABEL, FOLDER_NOT_IN_WORKSPACE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { isEqualOrParent } from 'vs/base/common/resources'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IHostService } from 'vs/workbench/services/host/browser/host'; @@ -62,7 +62,8 @@ export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICommandService private readonly _commandService: ICommandService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @IHostService private readonly _hostService: IHostService + @IHostService private readonly _hostService: IHostService, + @IUriIdentityService private readonly _uriIdentityService: IUriIdentityService ) { super(); } @@ -183,7 +184,7 @@ export class TerminalValidatedLocalLinkProvider extends TerminalBaseLinkProvider private _isDirectoryInsideWorkspace(uri: URI) { const folders = this._workspaceContextService.getWorkspace().folders; for (let i = 0; i < folders.length; i++) { - if (isEqualOrParent(uri, folders[i].uri)) { + if (this._uriIdentityService.extUri.isEqualOrParent(uri, folders[i].uri)) { return true; } } diff --git a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts index cc4de710a0a..d25b3982fcc 100644 --- a/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remoteTerminalChannel.ts @@ -7,7 +7,7 @@ import { Event } from 'vs/base/common/event'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -191,7 +191,7 @@ export class RemoteTerminalChannelClient { constructor( private readonly _remoteAuthority: string, private readonly _channel: IChannel, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkbenchConfigurationService private readonly _configurationService: IWorkbenchConfigurationService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IConfigurationResolverService private readonly _resolverService: IConfigurationResolverService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, @@ -211,6 +211,9 @@ export class RemoteTerminalChannelClient { } public async createTerminalProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + // Be sure to first wait for the remote configuration + await this._configurationService.whenRemoteConfigurationLoaded(); + const terminalConfig = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); const configuration: ICompleteTerminalConfiguration = { 'terminal.integrated.automationShell.windows': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.windows'), diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 438b3cad3c7..8a1aa13b2d8 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from 'vs/base/common/actions'; -import { toErrorMessage } from 'vs/base/common/errorMessage'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -258,7 +257,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo await this.userDataSyncService.accept(syncResource, conflict.remoteResource, undefined, this.userDataAutoSyncEnablementService.isEnabled()); } } catch (e) { - this.notificationService.error(e); + this.notificationService.error(localize('accept failed', "Error while accepting changes. Please check [logs]({0}) for more details.", `command:${SHOW_SYNC_LOG_COMMAND_ID}`)); } } @@ -268,7 +267,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo await this.userDataSyncService.accept(syncResource, conflict.localResource, undefined, this.userDataAutoSyncEnablementService.isEnabled()); } } catch (e) { - this.notificationService.error(e); + this.notificationService.error(localize('accept failed', "Error while accepting changes. Please check [logs]({0}) for more details.", `command:${SHOW_SYNC_LOG_COMMAND_ID}`)); } } @@ -1050,7 +1049,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo await that.turnOff(); } catch (e) { if (!isPromiseCanceledError(e)) { - that.notificationService.error(localize('turn off failed', "Error while turning off sync: {0}", toErrorMessage(e))); + that.notificationService.error(localize('turn off failed', "Error while turning off Settings Sync. Please check [logs]({0}) for more details.", `command:${SHOW_SYNC_LOG_COMMAND_ID}`)); } } } @@ -1254,7 +1253,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); } } else { - this.notificationService.error(e); + this.notificationService.error(localize('accept failed', "Error while accepting changes. Please check [logs]({0}) for more details.", `command:${SHOW_SYNC_LOG_COMMAND_ID}`)); } } } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts index b53ddab8504..8f80d2b98bb 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts @@ -44,7 +44,7 @@ if (product.quality !== 'stable') { properties: { 'workbench.experimental.gettingStarted': { type: 'boolean', - description: localize('gettingStartedDescription', "Enables an experimental Getting Started page, accesible via the Help menu."), + description: localize('gettingStartedDescription', "Enables an experimental Getting Started page, available via the Help menu."), default: false, } } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css index 8e16b49a24c..7c199b89de8 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css @@ -16,30 +16,34 @@ left: 0; } -.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .header { - text-align: center; +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories { + height: calc(100% - 50px); + display: flex; + flex-direction: column; } -.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .title { - display: inline-block; +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .gap { + flex: 150px 0 1 +} + +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .header { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-title { - margin-top: 6px; - margin-bottom: 2px; + margin-bottom: 4px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-description-container { width: 100% } -.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-description { - font-size: 12pt; -} - .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories .category-progress { - margin-top: 6px; - font-size: 8pt; + margin-top: 12px; + font-size: 12px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.categories progress { @@ -51,14 +55,14 @@ display: flex; flex-wrap: wrap; justify-content: center; - width: 70%; max-width: 900px; - margin: 20px auto; + margin: 32px auto; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .getting-started-category { width: 330px; - min-height: 100px; + min-height: 80px; + margin: 12px; text-align: left; display: flex; } @@ -68,21 +72,20 @@ } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .getting-started-category .codicon { - margin: 10px 8px 0 0; + margin-right: 10px; font-size: 32px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category { width: 330px; - height: 100px; display: flex; + padding: 40px 0 20px; margin-left: 12px; - + min-height: auto; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-category .codicon { margin-left:0; - margin-top: 28px; font-size: 22pt; } @@ -90,39 +93,45 @@ display: flex; height: 100%; justify-content: center; - padding: 30px; + padding: 44px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task { + display: flex; width: 100%; - height: 26pt; overflow: hidden; } +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task:not(.expanded) .task-description, +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task:not(.expanded) .actions, +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task:not(.expanded) button, +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task:not(.expanded) a { + display: none; +} + .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task.expanded { width: 100%; height: unset; } -.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-description-container { - padding-left: 30px; - padding-right: 4px; -} - .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-title { - margin-bottom: 4px; font-size: 14pt; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-description { - font-size: 11pt; + margin-top: 4px; + font-size: 13px; +} + +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .actions { + margin-top: 12px; + display: flex; + align-items: center; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .task-next { - float: right; - margin-top: 16px; - margin-right: 25px; + margin-left: auto; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon.hidden { @@ -130,39 +139,46 @@ } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task .codicon { - float: left; - font-size: 20pt; - position: relative; - top: -4px; - left: -1px; + margin-right: 8px; + font-size: 20px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail .getting-started-task-action { - margin: 10px 0 0; - padding: 4px 8px; + padding: 6px 12px; font-size: 11pt; min-width: 100px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail #getting-started-detail-left { min-width: 330px; - width: 33%; + width: 40%; max-width: 400px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide.detail #getting-started-detail-right { width: 66%; text-align: center; - padding: 35px; + padding-left: 44px; margin-bottom: 50px; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button { border: none; - margin: 10px; color: inherit; text-align: left; - padding: 10px; + padding: 16px; +} + +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button:not(:first-child) { + margin-top: 12px; +} + +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button:hover { + cursor: pointer; +} + +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer button:focus { + outline-style: solid; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .prev-button { @@ -172,21 +188,15 @@ margin: 10px; } +.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .prev-button:hover { + cursor: pointer; +} + .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .prev-button .codicon { position: relative; top: 2px; } -.monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .product-icon { - background-image: url('../../../../browser/media/code-icon.svg'); - width: 75px; - height: 75px; - background-size: contain; - display: inline-block; - margin-right: 20px; -} - - .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .skip { display: block; width: 150px; @@ -195,25 +205,28 @@ } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h1 { - font-size: 32pt; + font-size: 32px; font-weight: normal; border-bottom: none; - margin-bottom: 0; + margin: 0; + padding: 0; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h2 { font-weight: normal; + margin: 0 0 4px 0; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide h3 { - font-weight: normal; - margin-top: 0; - margin-bottom: 0; + font-size: 18px; + font-weight: bold; + margin: 0; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .gettingStartedSlide .subtitle { - font-size: 18pt; - margin-top: 0; + font-size: 16px; + margin: 0; + padding: 0; } .monaco-workbench .part.editor > .content .walkThroughContent .gettingStartedContainer .footer { diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index ad2d3527c2a..637330e06db 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -176,11 +176,14 @@ export class GettingStartedPage extends Disposable { } this.editorInput.selectedTask = id; const taskToExpand = assertIsDefined(this.currentCategory.content.items.find(task => task.id === id)); - mediaElement.setAttribute('src', taskToExpand.media.toString()); + + mediaElement.setAttribute('src', taskToExpand.media.path.toString()); + mediaElement.setAttribute('alt', taskToExpand.media.altText); taskElement.parentElement?.querySelectorAll('.expanded').forEach(node => node.classList.remove('expanded')); taskElement.classList.add('expanded'); } else { mediaElement.setAttribute('src', ''); + mediaElement.setAttribute('alt', ''); } } @@ -284,23 +287,24 @@ export class GettingStartedPage extends Disposable { const categoryElements = category.content.items.map( (task, i, arr) => $('button.getting-started-task', { 'x-dispatch': 'selectTask:' + task.id, id: 'getting-started-task-' + task.id }, - $('.codicon' + (task.done ? '.codicon-star-full' : '.codicon-star-empty'), { id: 'done-task-' + task.id }), + $('.codicon' + (task.done ? '.codicon-pass-filled' : '.codicon-circle-large-outline'), { id: 'done-task-' + task.id }), $('.task-description-container', {}, $('h3.task-title', {}, task.title), $('.task-description.description', {}, task.description), - ...( - task.button - ? [$('button.emphasis.getting-started-task-action', { 'x-dispatch': 'runTaskAction:' + task.id }, - task.button.title + this.getKeybindingLabel(task.button.command) - )] - : []), - ...( - arr[i + 1] - ? [ - $('a.task-next', - { 'x-dispatch': 'selectTask:' + arr[i + 1].id }, localize('next', "Next")), - ] : [] - ) + $('.actions', {}, + ...( + task.button + ? [$('button.emphasis.getting-started-task-action', { 'x-dispatch': 'runTaskAction:' + task.id }, + task.button.title + this.getKeybindingLabel(task.button.command) + )] + : []), + ...( + arr[i + 1] + ? [ + $('a.task-next', + { 'x-dispatch': 'selectTask:' + arr[i + 1].id }, localize('next', "Next")), + ] : [] + )) ))); const detailContainer = assertIsDefined(document.getElementById('getting-started-detail-container')); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started.ts index d002501a8ef..dd62545272f 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/vs_code_editor_getting_started.ts @@ -10,14 +10,13 @@ export default () => `
+
-
-
-

${escape(localize('gettingStarted.vscode', "Visual Studio Code"))}

-

${escape(localize({ key: 'gettingStarted.editingRedefined', comment: ['Shown as subtitle on the Welcome page.'] }, "Code editing. Redefined"))}

-
+

${escape(localize('gettingStarted.vscode', "Visual Studio Code"))}

+

${escape(localize({ key: 'gettingStarted.editingRedefined', comment: ['Shown as subtitle on the Welcome page.'] }, "Code editing. Redefined"))}

+
= this._register(new Emitter()); public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; + private readonly _onDidInitialize = this._register(new Emitter()); + public readonly onDidInitialize = this._onDidInitialize.event; + constructor( remoteAuthority: string, configurationCache: IConfigurationCache, @@ -205,6 +208,7 @@ export class RemoteUserConfiguration extends Disposable { this._userConfiguration.dispose(); this._userConfiguration = userConfiguration; this.onDidUserConfigurationChange(configurationModel); + this._onDidInitialize.fire(configurationModel); } }); } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 7a1b2cd45b1..0a00161659e 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -12,9 +12,9 @@ import { Queue, Barrier, runWhenIdle } from 'vs/base/common/async'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; -import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationService, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, isSingleFolderWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, IEmptyWorkspaceInitializationPayload, useSlashForPath, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; @@ -38,11 +38,12 @@ class Workspace extends BaseWorkspace { initialized: boolean = false; } -export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { +export class WorkspaceService extends Disposable implements IWorkbenchConfigurationService, IWorkspaceContextService { public _serviceBrand: undefined; private workspace!: Workspace; + private initRemoteUserConfigurationBarrier: Barrier; private completeWorkspaceBarrier: Barrier; private readonly configurationCache: IConfigurationCache; private _configuration: Configuration; @@ -93,6 +94,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); } + this.initRemoteUserConfigurationBarrier = new Barrier(); this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; @@ -104,9 +106,16 @@ export class WorkspaceService extends Disposable implements IConfigurationServic this.localUserConfiguration = this._register(new UserConfiguration(environmentService.settingsResource, remoteAuthority ? LOCAL_MACHINE_SCOPES : undefined, fileService)); this._register(this.localUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onLocalUserConfigurationChanged(userConfiguration))); if (remoteAuthority) { - this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, fileService, remoteAgentService)); - this._register(this.remoteUserConfiguration.onDidChangeConfiguration(userConfiguration => this.onRemoteUserConfigurationChanged(userConfiguration))); + const remoteUserConfiguration = this.remoteUserConfiguration = this._register(new RemoteUserConfiguration(remoteAuthority, configurationCache, fileService, remoteAgentService)); + this._register(remoteUserConfiguration.onDidInitialize(remoteUserConfigurationModel => { + this._register(remoteUserConfiguration.onDidChangeConfiguration(remoteUserConfigurationModel => this.onRemoteUserConfigurationChanged(remoteUserConfigurationModel))); + this.onRemoteUserConfigurationChanged(remoteUserConfigurationModel); + this.initRemoteUserConfigurationBarrier.open(); + })); + } else { + this.initRemoteUserConfigurationBarrier.open(); } + this.workspaceConfiguration = this._register(new WorkspaceConfiguration(configurationCache, fileService)); this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => { this.onWorkspaceConfigurationChanged().then(() => { @@ -208,7 +217,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic // Recompute current workspace folders if we have folders to add const workspaceConfigPath = this.getWorkspace().configuration!; const workspaceConfigFolder = dirname(workspaceConfigPath); - currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigPath); + currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigPath, this.uriIdentityService.extUri); const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri); const storedFoldersToAdd: IStoredWorkspaceFolder[] = []; @@ -224,7 +233,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic continue; } } catch (e) { /* Ignore */ } - storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, false, folderToAdd.name, workspaceConfigFolder, slashForPath)); + storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, false, folderToAdd.name, workspaceConfigFolder, slashForPath, this.uriIdentityService.extUri)); } // Apply to array of newStoredFolders @@ -336,6 +345,10 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this._configuration.keys(); } + public async whenRemoteConfigurationLoaded(): Promise { + await this.initRemoteUserConfigurationBarrier.wait(); + } + async initialize(arg: IWorkspaceInitializationPayload): Promise { mark('willInitWorkspaceService'); @@ -373,7 +386,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return this.workspaceConfiguration.initialize({ id: workspaceIdentifier.id, configPath: workspaceIdentifier.configPath }) .then(() => { const workspaceConfigPath = workspaceIdentifier.configPath; - const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), workspaceConfigPath); + const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), workspaceConfigPath, this.uriIdentityService.extUri); const workspaceId = workspaceIdentifier.id; const workspace = new Workspace(workspaceId, workspaceFolders, workspaceConfigPath, uri => this.uriIdentityService.extUri.ignorePathCasing(uri)); workspace.initialized = this.workspaceConfiguration.initialized; @@ -587,7 +600,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private async onWorkspaceConfigurationChanged(): Promise { if (this.workspace && this.workspace.configuration) { - let newFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration); + let newFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration, this.uriIdentityService.extUri); // Validate only if workspace is initialized if (this.workspace.initialized) { diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index edec7c0032b..e54d141f447 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -5,6 +5,8 @@ import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const FOLDER_CONFIG_FOLDER_NAME = '.vscode'; export const FOLDER_SETTINGS_NAME = 'settings'; @@ -43,4 +45,13 @@ export interface IConfigurationCache { } +export const IWorkbenchConfigurationService = createDecorator('configurationService'); +export interface IWorkbenchConfigurationService extends IConfigurationService { + /** + * A promise that resolves when the remote configuration is loaded in a remote window. + * The promise is resolved immediately if the window is not remote. + */ + whenRemoteConfigurationLoaded(): Promise; +} + export const TASKS_DEFAULT = '{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": []\n}'; diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 222e877c68b..1cd34887e54 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -283,10 +283,10 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const filter: IFilter = { name: languageName, extensions: distinct(extensions).slice(0, 10).map(e => trim(e, '.')) }; - if (ext && extensions.indexOf(ext) >= 0) { + if (ext && extensions.indexOf(ext) >= 0 && !matchingFilter) { matchingFilter = filter; - return null; // matching filter will be added last to the top + return null; // first matching filter will be added to the top } return filter; diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index ff3fe29fd60..e0eca9ebe37 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -221,7 +221,7 @@ export class SimpleFileDialog { } private getScheme(available: readonly string[] | undefined, defaultUri: URI | undefined): string { - if (available) { + if (available && available.length > 0) { if (defaultUri && (available.indexOf(defaultUri.scheme) >= 0)) { return defaultUri.scheme; } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 458e5e41119..bd176bffd4c 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -11,7 +11,7 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn import { Registry } from 'vs/platform/registry/common/platform'; import { ResourceMap } from 'vs/base/common/map'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { Schemas } from 'vs/base/common/network'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; @@ -251,8 +251,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (this.uriIdentityService.extUri.isEqual(source, resource)) { targetResource = target; // file got moved } else { - const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive); - const index = indexOfPath(resource.path, source.path, ignoreCase); + const index = indexOfPath(resource.path, source.path, this.uriIdentityService.extUri.ignorePathCasing(resource)); targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts index 4a1b9272761..9498ec70955 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionBisect.ts @@ -47,21 +47,18 @@ class BisectState { try { interface Raw extends BisectState { } const data: Raw = JSON.parse(raw); - return new BisectState(data.extensions, data.low, data.high); + return new BisectState(data.extensions, data.low, data.high, data.mid); } catch { return undefined; } } - readonly mid: number; - constructor( readonly extensions: string[], readonly low: number, readonly high: number, - ) { - this.mid = ((low + high) / 2) | 0; - } + readonly mid: number = ((low + high) / 2) | 0 + ) { } } class ExtensionBisectService implements IExtensionBisectService { @@ -111,7 +108,7 @@ class ExtensionBisectService implements IExtensionBisectService { throw new Error('invalid state'); } const extensionIds = extensions.map(ext => ext.identifier.id); - const newState = new BisectState(extensionIds, 0, extensionIds.length); + const newState = new BisectState(extensionIds, 0, extensionIds.length, 0); this._storageService.store(ExtensionBisectService._storageKey, JSON.stringify(newState), StorageScope.GLOBAL, StorageTarget.MACHINE); await this._storageService.flush(); } @@ -120,6 +117,10 @@ class ExtensionBisectService implements IExtensionBisectService { if (!this._state) { throw new Error('invalid state'); } + // check if bad when all extensions are disabled + if (seeingBad && this._state.mid === 0 && this._state.high === this._state.extensions.length) { + return { bad: true, id: '' }; + } // check if there is only one left if (this._state.low === this._state.high - 1) { await this.reset(); @@ -215,7 +216,7 @@ registerAction2(class extends Action2 { const res = await dialogService.confirm({ message: localize('msg.start', "Extension Bisect"), - detail: localize('detail.start', "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", 1 + Math.log2(extensions.length) | 0), + detail: localize('detail.start', "Extension Bisect will use binary search to find an extension that causes a problem. During the process the window reloads repeatedly (~{0} times). Each time you must confirm if you are still seeing problems.", 2 + Math.log2(extensions.length) | 0), primaryButton: localize('msg2', "Start Extension Bisect") }); @@ -249,7 +250,11 @@ registerAction2(class extends Action2 { return; } if (seeingBad === undefined) { - seeingBad = await this._checkForBad(dialogService); + const goodBadStopCancel = await this._checkForBad(dialogService, bisectService); + if (goodBadStopCancel === null) { + return; + } + seeingBad = goodBadStopCancel; } if (seeingBad === undefined) { await bisectService.reset(); @@ -265,7 +270,7 @@ registerAction2(class extends Action2 { if (done.bad) { // DONE but nothing found await dialogService.show(Severity.Info, localize('done.msg', "Extension Bisect"), [], { - detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}", productService.nameShort) + detail: localize('done.detail2', "Extension Bisect is done but no extension has been identified. This might be a problem with {0}.", productService.nameShort) }); } else { @@ -290,21 +295,23 @@ registerAction2(class extends Action2 { hostService.reload(); } - private async _checkForBad(dialogService: IDialogService) { + private async _checkForBad(dialogService: IDialogService, bisectService: IExtensionBisectService): Promise { const options = { - cancelId: 2, - detail: localize('detail.next', "Are you still seeing the problem for which you have started extension bisect?") + cancelId: 3, + detail: localize('bisect', "Extension Bisect is active and has disabled {0} extensions. Check if you can still reproduce the problem and proceed by selecting from these options.", bisectService.disabledCount), }; const res = await dialogService.show( Severity.Info, localize('msg.next', "Extension Bisect"), - [localize('next.good', "Good now"), localize('next.bad', "This is bad"), localize('next.stop', "Stop Bisect")], + [localize('next.good', "Good now"), localize('next.bad', "This is bad"), localize('next.stop', "Stop Bisect"), localize('next.cancel', "Cancel")], options ); - if (res.choice === options.cancelId) { - return undefined; + switch (res.choice) { + case 0: return false; //good now + case 1: return true; //bad + case 2: return undefined; //stop } - return res.choice === 1; + return null; //cancel } }); diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index eb5e75f1f64..94d47c9cdec 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -215,14 +215,16 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } if (extensionKind === 'web') { - const enableLocalWebWorker = this.configurationService.getValue(webWorkerExtHostConfig); - if (enableLocalWebWorker) { - // Web extensions are enabled on all configurations - return false; - } - if (this.extensionManagementServerService.localExtensionManagementServer === null) { - // Web extensions run only in the web - return false; + if (this.extensionManagementServerService.webExtensionManagementServer) { + if (server === this.extensionManagementServerService.webExtensionManagementServer) { + return false; + } + } else if (server === this.extensionManagementServerService.localExtensionManagementServer) { + const enableLocalWebWorker = this.configurationService.getValue(webWorkerExtHostConfig); + if (enableLocalWebWorker) { + // Web extensions are enabled on all configurations + return false; + } } } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 7f6e33de946..380898ba50d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -97,7 +97,7 @@ export interface IWebExtensionsScannerService { scanExtensions(type?: ExtensionType): Promise; scanAndTranslateExtensions(type?: ExtensionType): Promise; scanAndTranslateSingleExtension(extensionLocation: URI, extensionType: ExtensionType): Promise; - canAddExtension(galleryExtension: IGalleryExtension): Promise; + canAddExtension(galleryExtension: IGalleryExtension): boolean; addExtension(galleryExtension: IGalleryExtension): Promise; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index db084cc3f1b..f0cc1bc865a 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -238,12 +238,12 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }; } - async canAddExtension(galleryExtension: IGalleryExtension): Promise { + canAddExtension(galleryExtension: IGalleryExtension): boolean { return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource; } async addExtension(galleryExtension: IGalleryExtension): Promise { - if (!(await this.canAddExtension(galleryExtension))) { + if (!this.canAddExtension(galleryExtension)) { const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; throw error; diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index ec8f1aca4ef..7259d0698d3 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -545,36 +545,48 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true); }); - test('test web extension on local server is disabled by kind', async () => { + test('test web extension on local server is disabled by kind when web worker is not enabled', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`) }); + (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false }); testObject = new TestExtensionEnablementService(instantiationService); - assert.ok(!testObject.isEnabled(localWorkspaceExtension)); + assert.equal(testObject.isEnabled(localWorkspaceExtension), false); assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); - test('test web extension on remote server is not disabled by kind when there is no local server', async () => { - instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); - const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) }); + test('test web extension on local server is not disabled by kind when web worker is enabled', async () => { + instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`) }); + (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true }); testObject = new TestExtensionEnablementService(instantiationService); - assert.ok(testObject.isEnabled(localWorkspaceExtension)); + assert.equal(testObject.isEnabled(localWorkspaceExtension), true); assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); }); - test('test web extension with no server is not disabled by kind when there is no local server', async () => { - instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); - const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) }); + test('test web extension on remote server is disabled by kind when web worker is not enabled', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); + (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: false }); testObject = new TestExtensionEnablementService(instantiationService); - assert.ok(testObject.isEnabled(localWorkspaceExtension)); - assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); + assert.equal(testObject.isEnabled(localWorkspaceExtension), false); + assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); }); - test('test web extension with no server is not disabled by kind when there is no local and remote server', async () => { - instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, null, anExtensionManagementServer('web', instantiationService))); - const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.https }) }); + test('test web extension on remote server is disabled by kind when web worker is enabled', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); + const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'vscode-remote' }) }); + (instantiationService.get(IConfigurationService)).setUserConfiguration('extensions', { webWorker: true }); testObject = new TestExtensionEnablementService(instantiationService); - assert.ok(testObject.isEnabled(localWorkspaceExtension)); - assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally); + assert.equal(testObject.isEnabled(localWorkspaceExtension), false); + assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind); + }); + + test('test web extension on web server is not disabled by kind', async () => { + instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(anExtensionManagementServer('vscode-local', instantiationService), anExtensionManagementServer('vscode-remote', instantiationService), anExtensionManagementServer('web', instantiationService))); + const webExtension = aLocalExtension2('pub.a', { extensionKind: ['web'] }, { location: URI.file(`pub.a`).with({ scheme: 'web' }) }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.isEnabled(webExtension), true); + assert.deepEqual(testObject.getEnablementState(webExtension), EnablementState.EnabledGlobally); }); }); @@ -598,7 +610,7 @@ function anExtensionManagementServerService(localExtensionManagementServer: IExt _serviceBrand: undefined, localExtensionManagementServer, remoteExtensionManagementServer, - webExtensionManagementServer: null, + webExtensionManagementServer, getExtensionManagementServer: (extension: IExtension) => { if (extension.location.scheme === Schemas.file) { return localExtensionManagementServer; diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index c454bf39efa..f261f99b59f 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -305,6 +305,11 @@ export const schema: IJSONSchema = { body: 'onCustomEditor:${9:viewType}', description: nls.localize('vscode.extension.activationEvents.onCustomEditor', 'An activation event emitted whenever the specified custom editor becomes visible.'), }, + { + label: 'onNotebook', + body: 'onNotebook:${10:viewType}', + description: nls.localize('vscode.extension.activationEvents.onNotebook', 'An activation event emitted whenever the specified notebook document is opened.'), + }, { label: '*', description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'), diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 5ec4c4bbae8..34fa2c3b059 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -45,6 +45,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { isUUID } from 'vs/base/common/uuid'; import { join } from 'vs/base/common/path'; +import { Readable, Writable } from 'stream'; +import { StringDecoder } from 'string_decoder'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; @@ -55,6 +57,11 @@ export interface ILocalProcessExtensionHostDataProvider { getInitData(): Promise; } +const enum NativeLogMarkers { + Start = 'START_NATIVE_LOG', + End = 'END_NATIVE_LOG', +} + export class LocalProcessExtensionHost implements IExtensionHost { public readonly kind = ExtensionHostKind.LocalProcess; @@ -151,13 +158,12 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._messageProtocol = Promise.all([ this._tryListenOnPipe(), this._tryFindDebugPort() - ]).then(data => { - const pipeName = data[0]; - const portNumber = data[1]; + ]).then(([pipeName, portNumber]) => { const env = objects.mixin(objects.deepClone(process.env), { AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess', PIPE_LOGGING: 'true', VERBOSE_LOGGING: true, + VSCODE_LOG_NATIVE: this._isExtensionDevHost, VSCODE_IPC_HOOK_EXTHOST: pipeName, VSCODE_HANDLES_UNCAUGHT_ERRORS: true, VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || this._productService.quality !== 'stable' || this._environmentService.verbose), @@ -225,13 +231,11 @@ export class LocalProcessExtensionHost implements IExtensionHost { // Catch all output coming from the extension host process type Output = { data: string, format: string[] }; - this._extensionHostProcess.stdout!.setEncoding('utf8'); - this._extensionHostProcess.stderr!.setEncoding('utf8'); - const onStdout = Event.fromNodeEventEmitter(this._extensionHostProcess.stdout!, 'data'); - const onStderr = Event.fromNodeEventEmitter(this._extensionHostProcess.stderr!, 'data'); + const onStdout = this._handleProcessOutputStream(this._extensionHostProcess.stdout!); + const onStderr = this._handleProcessOutputStream(this._extensionHostProcess.stderr!); const onOutput = Event.any( - Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] })), - Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] })) + Event.map(onStdout.event, o => ({ data: `%c${o}`, format: [''] })), + Event.map(onStderr.event, o => ({ data: `%c${o}`, format: ['color: red'] })) ); // Debounce all output, so we can render it in the Chrome console as a group @@ -497,11 +501,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { // Send to local console log(entry, 'Extension Host'); - - // Broadcast to other windows if we are in development mode - if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) { - this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry); - } } } @@ -525,6 +524,44 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._onExit.fire([code, signal]); } + private _handleProcessOutputStream(stream: Readable) { + let last = ''; + let isOmitting = false; + const event = new Emitter(); + const decoder = new StringDecoder('utf-8'); + stream.pipe(new Writable({ + write(chunk, _encoding, callback) { + // not a fancy approach, but this is the same approach used by the split2 + // module which is well-optimized (https://github.com/mcollina/split2) + last += typeof chunk === 'string' ? chunk : decoder.write(chunk); + let lines = last.split(/\r?\n/g); + last = lines.pop()!; + + // protected against an extension spamming and leaking memory if no new line is written. + if (last.length > 10_000) { + lines.push(last); + last = ''; + } + + for (const line of lines) { + if (isOmitting) { + if (line === NativeLogMarkers.End) { + isOmitting = false; + } + } else if (line === NativeLogMarkers.Start) { + isOmitting = true; + } else if (line.length) { + event.fire(line + '\n'); + } + } + + callback(); + } + })); + + return event; + } + public async enableInspectPort(): Promise { if (typeof this._inspectPort === 'number') { return true; diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 3050bc6e2d6..80177b1fad9 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -498,7 +498,7 @@ async function readWindowsCaCertificates() { const winCA = await import('vscode-windows-ca-certs'); let ders: any[] = []; - const store = winCA(); + const store = new winCA.Crypt32(); try { let der: any; while (der = store.next()) { diff --git a/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html b/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html index 392b4b4aeaa..67560d96e6d 100644 --- a/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html +++ b/src/vs/workbench/services/extensions/worker/httpWebWorkerExtensionHostIframe.html @@ -1,7 +1,7 @@ - +