diff --git a/.yarnrc b/.yarnrc index 861ab1b26ec..6b1a011a28a 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,4 +1,4 @@ disturl "https://electronjs.org/headers" -target "17.4.0" +target "17.4.1" runtime "electron" build_from_source "true" diff --git a/build/azure-pipelines/common/installPlaywright.js b/build/azure-pipelines/common/installPlaywright.js index 15f5898e7f7..af4bd5fb54c 100644 --- a/build/azure-pipelines/common/installPlaywright.js +++ b/build/azure-pipelines/common/installPlaywright.js @@ -5,7 +5,7 @@ *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const retry_1 = require("./retry"); -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/utils/registry'); +const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); async function install() { await (0, retry_1.retry)(() => installDefaultBrowsersForNpmInstall()); } diff --git a/build/azure-pipelines/common/installPlaywright.ts b/build/azure-pipelines/common/installPlaywright.ts index a99ab3aeec1..5d837a55413 100644 --- a/build/azure-pipelines/common/installPlaywright.ts +++ b/build/azure-pipelines/common/installPlaywright.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { retry } from './retry'; -const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/utils/registry'); +const { installDefaultBrowsersForNpmInstall } = require('playwright-core/lib/server'); async function install() { await retry(() => installDefaultBrowsersForNpmInstall()); diff --git a/build/azure-pipelines/darwin/product-build-darwin-test.yml b/build/azure-pipelines/darwin/product-build-darwin-test.yml new file mode 100644 index 00000000000..e28da6547eb --- /dev/null +++ b/build/azure-pipelines/darwin/product-build-darwin-test.yml @@ -0,0 +1,274 @@ +steps: + - task: NodeTool@0 + inputs: + versionSpec: "16.x" + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode + SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: Compilation + path: $(Build.ArtifactStagingDirectory) + displayName: Download compilation output + + - script: | + set -e + tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz + displayName: Extract compilation output + + # Set up the credentials to retrieve distro repo and setup git persona + # to create a merge commit for when we merge distro into oss + - script: | + set -e + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + displayName: Prepare tooling + + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + + - script: | + set -e + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") + displayName: Merge distro + + - script: | + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js $VSCODE_ARCH $ENABLE_TERRAPIN > .build/yarnlockhash + displayName: Prepare yarn cache flags + + - task: Cache@2 + inputs: + key: "nodeModules | $(Agent.OS) | .build/yarnlockhash" + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: | + set -e + tar -xzf .build/node_modules_cache/cache.tgz + condition: and(succeeded(), eq(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Extract node_modules cache + + - script: | + set -e + npm install -g node-gyp@latest + node-gyp --version + displayName: Update node-gyp + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - script: | + set -e + npx https://aka.ms/enablesecurefeed standAlone + timeoutInMinutes: 5 + retryCountOnTaskFailure: 3 + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true'), eq(variables['ENABLE_TERRAPIN'], 'true')) + displayName: Switch to Terrapin packages + + - script: | + set -e + export npm_config_arch=$(VSCODE_ARCH) + export npm_config_node_gyp=$(which node-gyp) + + for i in {1..3}; do # try 3 times, for Terrapin + yarn --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." + done + env: + ELECTRON_SKIP_BINARY_DOWNLOAD: 1 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + GITHUB_TOKEN: "$(github-distro-mixin-password)" + displayName: Install dependencies + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + + - script: | + set -e + node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + mkdir -p .build/node_modules_cache + tar -czf .build/node_modules_cache/cache.tgz --files-from .build/node_modules_list.txt + condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) + displayName: Create node_modules archive + + # This script brings in the right resources (images, icons, etc) based on the quality (insiders, stable, exploration) + - script: | + set -e + node build/azure-pipelines/mixin + displayName: Mix in quality + + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-darwin-$(VSCODE_ARCH)-min-ci + displayName: Build client + + - script: | + set -e + node build/azure-pipelines/mixin --server + displayName: Mix in server quality + + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-darwin-$(VSCODE_ARCH)-min-ci + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci + displayName: Build Server + + - script: | + set -e + VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ + yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + displayName: Download Electron and Playwright + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + + # Setting hardened entitlements is a requirement for: + # * Running tests on Big Sur (because Big Sur has additional security precautions) + - script: | + set -e + security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain + security default-keychain -s $(agent.tempdirectory)/buildagent.keychain + security unlock-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain + echo "$(macos-developer-certificate)" | base64 -D > $(agent.tempdirectory)/cert.p12 + security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain + VSCODE_ARCH=$(VSCODE_ARCH) DEBUG=electron-osx-sign* node build/darwin/sign.js + displayName: Set Hardened Entitlements + + - script: | + set -e + ./scripts/test.sh --build --tfs "Unit Tests" + displayName: Run unit tests (Electron) + timeoutInMinutes: 15 + + - script: | + set -e + yarn test-node --build + displayName: Run unit tests (node.js) + timeoutInMinutes: 15 + + - script: | + set -e + DEBUG=*browser* yarn test-browser-no-install --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" + displayName: Run unit tests (Browser, Chromium & Webkit) + timeoutInMinutes: 30 + + - script: | + # Figure out the full absolute path of the product we just built + # including the remote server and configure the integration tests + # to run with these builds instead of running out of sources. + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ + ./scripts/test-integration.sh --build --tfs "Integration Tests" + displayName: Run integration tests (Electron) + timeoutInMinutes: 20 + + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ + ./scripts/test-web-integration.sh --browser webkit + displayName: Run integration tests (Browser, Webkit) + timeoutInMinutes: 20 + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ + ./scripts/test-remote-integration.sh + displayName: Run integration tests (Remote) + timeoutInMinutes: 20 + + - script: | + set -e + ps -ef + displayName: Diagnostics before smoke test run + continueOnError: true + condition: succeededOrFailed() + + - script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --web --tracing --headless + timeoutInMinutes: 10 + displayName: Run smoke tests (Browser, Chromium) + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 20 + displayName: Run smoke tests (Electron) + + - script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) + APP_NAME="`ls $APP_ROOT | head -n 1`" + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ + yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" + timeoutInMinutes: 20 + displayName: Run smoke tests (Remote) + + - script: | + set -e + ps -ef + displayName: Diagnostics after smoke test run + continueOnError: true + condition: succeededOrFailed() + + - task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-macos-$(VSCODE_ARCH) + targetPath: .build/crashes + displayName: "Publish Crash Reports" + continueOnError: true + condition: failed() + + # In order to properly symbolify above crash reports + # (if any), we need the compiled native modules too + - task: PublishPipelineArtifact@0 + inputs: + artifactName: node-modules-macos-$(VSCODE_ARCH) + targetPath: node_modules + displayName: "Publish Node Modules" + continueOnError: true + condition: failed() + + - task: PublishPipelineArtifact@0 + inputs: + artifactName: logs-macos-$(VSCODE_ARCH)-$(System.JobAttempt) + targetPath: .build/logs + displayName: "Publish Log Files" + continueOnError: true + condition: failed() + + - task: PublishTestResults@2 + displayName: Publish Tests Results + inputs: + testResultsFiles: "*-results.xml" + searchFolder: "$(Build.ArtifactStagingDirectory)/test-results" + condition: succeededOrFailed() diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml new file mode 100644 index 00000000000..1b8cfef6737 --- /dev/null +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -0,0 +1,95 @@ +steps: + - task: NodeTool@0 + inputs: + versionSpec: "16.x" + + - task: AzureKeyVault@1 + displayName: "Azure Key Vault: Get Secrets" + inputs: + azureSubscription: "vscode-builds-subscription" + KeyVaultName: vscode + SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" + + - script: | + set -e + cat << EOF > ~/.netrc + machine github.com + login vscode + password $(github-distro-mixin-password) + EOF + + git config user.email "vscode@microsoft.com" + git config user.name "VSCode" + displayName: Prepare tooling + + - script: | + set -e + git fetch https://github.com/$(VSCODE_MIXIN_REPO).git $VSCODE_DISTRO_REF + echo "##vso[task.setvariable variable=VSCODE_DISTRO_COMMIT;]$(git rev-parse FETCH_HEAD)" + git checkout FETCH_HEAD + condition: and(succeeded(), ne(variables.VSCODE_DISTRO_REF, ' ')) + displayName: Checkout override commit + + - script: | + set -e + git pull --no-rebase https://github.com/$(VSCODE_MIXIN_REPO).git $(node -p "require('./package.json').distro") + displayName: Merge distro + + - script: | + mkdir -p .build + node build/azure-pipelines/common/computeNodeModulesCacheKey.js x64 $ENABLE_TERRAPIN > .build/yarnlockhash + displayName: Prepare yarn cache flags + + - task: Cache@2 + inputs: + key: "nodeModules | $(Agent.OS) | .build/yarnlockhash" + path: .build/node_modules_cache + cacheHitVar: NODE_MODULES_RESTORED + displayName: Restore node_modules cache + + - script: | + set -e + tar -xzf .build/node_modules_cache/cache.tgz + displayName: Extract node_modules cache + + - script: | + set -e + node build/azure-pipelines/mixin + displayName: Mix in quality + + - download: current + artifact: unsigned_vscode_client_darwin_x64_archive + displayName: Download x64 artifact + + - download: current + artifact: unsigned_vscode_client_darwin_arm64_archive + displayName: Download arm64 artifact + + - script: | + set -e + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip + cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_arm64_archive/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip + unzip $(agent.builddirectory)/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 + unzip $(agent.builddirectory)/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/VSCode-darwin-arm64 + DEBUG=* node build/darwin/create-universal-app.js + displayName: Create Universal App + + - script: | + set -e + security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain + security default-keychain -s $(agent.tempdirectory)/buildagent.keychain + security unlock-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain + echo "$(macos-developer-certificate)" | base64 -D > $(agent.tempdirectory)/cert.p12 + security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain + VSCODE_ARCH=$(VSCODE_ARCH) DEBUG=electron-osx-sign* node build/darwin/sign.js + displayName: Set Hardened Entitlements + + - script: | + set -e + pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip * && popd + displayName: Archive build + + - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip + artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + displayName: Publish client archive diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 073932d7032..5c524dd42fb 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -15,16 +15,12 @@ steps: artifact: Compilation path: $(Build.ArtifactStagingDirectory) displayName: Download compilation output - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - script: | set -e tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz displayName: Extract compilation output - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - # Set up the credentials to retrieve distro repo and setup git persona - # to create a merge commit for when we merge distro into oss - script: | set -e cat << EOF > ~/.netrc @@ -122,7 +118,6 @@ steps: VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ yarn gulp vscode-darwin-$(VSCODE_ARCH)-min-ci displayName: Build client - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - script: | set -e @@ -136,34 +131,6 @@ steps: VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ yarn gulp vscode-reh-web-darwin-$(VSCODE_ARCH)-min-ci displayName: Build Server - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal')) - - - script: | - set -e - VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - yarn npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" - displayName: Download Electron and Playwright - condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'universal'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - download: current - artifact: unsigned_vscode_client_darwin_x64_archive - displayName: Download x64 artifact - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - - - download: current - artifact: unsigned_vscode_client_darwin_arm64_archive - displayName: Download arm64 artifact - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) - - - script: | - set -e - cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip $(agent.builddirectory)/VSCode-darwin-x64.zip - cp $(Pipeline.Workspace)/unsigned_vscode_client_darwin_arm64_archive/VSCode-darwin-arm64.zip $(agent.builddirectory)/VSCode-darwin-arm64.zip - unzip $(agent.builddirectory)/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 - unzip $(agent.builddirectory)/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/VSCode-darwin-arm64 - DEBUG=* node build/darwin/create-universal-app.js - displayName: Create Universal App - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'universal')) # Setting hardened entitlements is a requirement for: # * Apple notarization @@ -179,139 +146,10 @@ steps: VSCODE_ARCH=$(VSCODE_ARCH) DEBUG=electron-osx-sign* node build/darwin/sign.js displayName: Set Hardened Entitlements - - script: | - set -e - ./scripts/test.sh --build --tfs "Unit Tests" - displayName: Run unit tests (Electron) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - yarn test-node --build - displayName: Run unit tests (node.js) - timeoutInMinutes: 15 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - DEBUG=*browser* yarn test-browser-no-install --sequential --build --browser chromium --browser webkit --tfs "Browser Unit Tests" - displayName: Run unit tests (Browser, Chromium & Webkit) - timeoutInMinutes: 30 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - # Figure out the full absolute path of the product we just built - # including the remote server and configure the integration tests - # to run with these builds instead of running out of sources. - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-integration.sh --build --tfs "Integration Tests" - displayName: Run integration tests (Electron) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-web-integration.sh --browser webkit - displayName: Run integration tests (Browser, Webkit) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - INTEGRATION_TEST_ELECTRON_PATH="$APP_ROOT/$APP_NAME/Contents/MacOS/Electron" \ - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - ./scripts/test-remote-integration.sh - displayName: Run integration tests (Remote) - timeoutInMinutes: 20 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - ps -ef - displayName: Diagnostics before smoke test run - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --web --tracing --headless - timeoutInMinutes: 10 - displayName: Run smoke tests (Browser, Chromium) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - yarn smoketest-no-compile --tracing --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Electron) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) - APP_NAME="`ls $APP_ROOT | head -n 1`" - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-darwin-$(VSCODE_ARCH)" \ - yarn smoketest-no-compile --tracing --remote --build "$APP_ROOT/$APP_NAME" - timeoutInMinutes: 20 - displayName: Run smoke tests (Remote) - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - script: | - set -e - ps -ef - displayName: Diagnostics after smoke test run - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - task: PublishPipelineArtifact@0 - inputs: - artifactName: crash-dump-macos-$(VSCODE_ARCH) - targetPath: .build/crashes - displayName: "Publish Crash Reports" - continueOnError: true - condition: failed() - - # In order to properly symbolify above crash reports - # (if any), we need the compiled native modules too - - task: PublishPipelineArtifact@0 - inputs: - artifactName: node-modules-macos-$(VSCODE_ARCH) - targetPath: node_modules - displayName: "Publish Node Modules" - continueOnError: true - condition: failed() - - - task: PublishPipelineArtifact@0 - inputs: - artifactName: logs-macos-$(VSCODE_ARCH)-$(System.JobAttempt) - targetPath: .build/logs - displayName: "Publish Log Files" - continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - - task: PublishTestResults@2 - displayName: Publish Tests Results - inputs: - testResultsFiles: "*-results.xml" - searchFolder: "$(Build.ArtifactStagingDirectory)/test-results" - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | set -e pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH).zip * && popd displayName: Archive build - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - script: | set -e @@ -322,22 +160,18 @@ steps: # package Remote Extension Host (Web) pushd .. && mv vscode-reh-web-darwin-$(VSCODE_ARCH) vscode-server-darwin-$(VSCODE_ARCH)-web && zip -Xry vscode-server-darwin-$(VSCODE_ARCH)-web.zip vscode-server-darwin-$(VSCODE_ARCH)-web && popd displayName: Prepare to publish servers - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - publish: $(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH).zip artifact: unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive displayName: Publish client archive - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH).zip artifact: vscode_server_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish server archive - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - publish: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web.zip artifact: vscode_web_darwin_$(VSCODE_ARCH)_archive-unsigned displayName: Publish web server archive - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) - task: AzureCLI@2 inputs: @@ -367,21 +201,21 @@ steps: inputs: BuildDropPath: $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) PackageName: Visual Studio Code - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH)/_manifest displayName: Publish SBOM (client) artifact: vscode_client_darwin_$(VSCODE_ARCH)_sbom - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 displayName: Generate SBOM (server) inputs: BuildDropPath: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH) PackageName: Visual Studio Code Server - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) - publish: $(agent.builddirectory)/vscode-server-darwin-$(VSCODE_ARCH)/_manifest displayName: Publish SBOM (server) artifact: vscode_server_darwin_$(VSCODE_ARCH)_sbom - condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false'), ne(variables['VSCODE_ARCH'], 'universal')) + condition: and(succeeded(), ne(variables['VSCODE_PUBLISH'], 'false')) diff --git a/build/azure-pipelines/linux/product-build-linux-client.yml b/build/azure-pipelines/linux/product-build-linux-client.yml index 66b9e15e881..b6472b5e573 100644 --- a/build/azure-pipelines/linux/product-build-linux-client.yml +++ b/build/azure-pipelines/linux/product-build-linux-client.yml @@ -21,7 +21,7 @@ steps: artifact: reh_node_modules-$(VSCODE_ARCH) path: $(Build.ArtifactStagingDirectory) displayName: Download server build dependencies - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'armhf')) - script: | set -e @@ -148,7 +148,7 @@ steps: rm -rf remote/node_modules tar -xzf $(Build.ArtifactStagingDirectory)/reh_node_modules-$(VSCODE_ARCH).tar.gz --directory $(Build.SourcesDirectory)/remote displayName: Extract server node_modules output - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + condition: and(succeeded(), ne(variables['VSCODE_ARCH'], 'armhf')) - script: | set -e @@ -323,7 +323,7 @@ steps: targetPath: .build/logs displayName: "Publish Log Files" continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + condition: and(failed(), eq(variables['VSCODE_ARCH'], 'x64'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/build/azure-pipelines/linux/product-build-linux-server.yml b/build/azure-pipelines/linux/product-build-linux-server.yml index 94145131c37..07fa3e46496 100644 --- a/build/azure-pipelines/linux/product-build-linux-server.yml +++ b/build/azure-pipelines/linux/product-build-linux-server.yml @@ -10,6 +10,16 @@ steps: KeyVaultName: vscode SecretsFilter: "github-distro-mixin-password,ESRP-PKI,esrp-aad-username,esrp-aad-password" + - task: Docker@1 + displayName: "Pull Docker image" + inputs: + azureSubscriptionEndpoint: "vscode-builds-subscription" + azureContainerRegistry: vscodehub.azurecr.io + command: "Run an image" + imageName: "vscode-linux-build-agent:centos7-devtoolset8-arm64" + containerCommand: uname + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) + - script: | set -e cat << EOF > ~/.netrc @@ -45,21 +55,24 @@ steps: - script: | set -e - export npm_config_arch=$(NPM_ARCH) - - for i in {1..3}; do # try 3 times, for Terrapin - yarn --cwd remote --frozen-lockfile --check-files && break - if [ $i -eq 3 ]; then - echo "Yarn failed too many times" >&2 - exit 1 - fi - echo "Yarn failed $i, trying again..." - done + $(pwd)/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh displayName: Install dependencies env: GITHUB_TOKEN: "$(github-distro-mixin-password)" condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'x64')) + - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + displayName: Register Docker QEMU + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) + + - script: | + set -e + docker run -e VSCODE_QUALITY -e GITHUB_TOKEN -v $(pwd):/root/vscode -v ~/.netrc:/root/.netrc vscodehub.azurecr.io/vscode-linux-build-agent:centos7-devtoolset8-arm64 /root/vscode/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh + displayName: Install dependencies via qemu + env: + GITHUB_TOKEN: "$(github-distro-mixin-password)" + condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) + - script: | set -e tar -cz --ignore-failed-read -f $(Build.ArtifactStagingDirectory)/reh_node_modules-$(VSCODE_ARCH).tar.gz -C $(Build.SourcesDirectory)/remote node_modules diff --git a/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh b/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh index f849b3acef2..d2f62087661 100755 --- a/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh +++ b/build/azure-pipelines/linux/scripts/install-remote-dependencies.sh @@ -2,4 +2,13 @@ set -e echo "Installing remote dependencies" -(cd remote && rm -rf node_modules && yarn --verbose) +(cd remote && rm -rf node_modules) + +for i in {1..3}; do # try 3 times, for Terrapin + yarn --cwd remote --frozen-lockfile --check-files && break + if [ $i -eq 3 ]; then + echo "Yarn failed too many times" >&2 + exit 1 + fi + echo "Yarn failed $i, trying again..." +done diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index f6eee5e5761..f642d0a6805 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -118,7 +118,7 @@ variables: - name: VSCODE_STEP_ON_IT value: ${{ eq(parameters.VSCODE_STEP_ON_IT, true) }} - name: VSCODE_BUILD_MACOS_UNIVERSAL - value: ${{ and(eq(variables['VSCODE_PUBLISH'], true), eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} + value: ${{ and(eq(parameters.VSCODE_BUILD_MACOS, true), eq(parameters.VSCODE_BUILD_MACOS_ARM64, true), eq(parameters.VSCODE_BUILD_MACOS_UNIVERSAL, true)) }} - name: AZURE_CDN_URL value: https://az764295.vo.msecnd.net - name: AZURE_DOCUMENTDB_ENDPOINT @@ -129,6 +129,8 @@ variables: value: microsoft/vscode-distro - name: skipComponentGovernanceDetection value: true + - name: Codeql.SkipTaskAutoInjection + value: true resources: containers: @@ -205,6 +207,13 @@ stages: steps: - template: linux/product-build-linux-server.yml + - ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}: + - job: arm64 + variables: + VSCODE_ARCH: arm64 + steps: + - template: linux/product-build-linux-server.yml + - ${{ if and(eq(parameters.VSCODE_COMPILE_ONLY, false), eq(variables['VSCODE_BUILD_STAGE_LINUX'], true)) }}: - stage: Linux dependsOn: @@ -304,13 +313,19 @@ stages: BUILDSECMON_OPT_IN: true jobs: - ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}: - - job: macOS + - job: macOSTest timeoutInMinutes: 90 variables: VSCODE_ARCH: x64 steps: - - template: darwin/product-build-darwin.yml - - ${{ if ne(variables['VSCODE_PUBLISH'], 'false') }}: + - template: darwin/product-build-darwin-test.yml + - ${{ if eq(variables['VSCODE_CIBUILD'], false) }}: + - job: macOS + timeoutInMinutes: 90 + variables: + VSCODE_ARCH: x64 + steps: + - template: darwin/product-build-darwin.yml - job: macOSSign dependsOn: - macOS @@ -327,7 +342,7 @@ stages: VSCODE_ARCH: arm64 steps: - template: darwin/product-build-darwin.yml - - ${{ if ne(variables['VSCODE_PUBLISH'], 'false') }}: + - ${{ if eq(variables['VSCODE_CIBUILD'], false) }}: - job: macOSARM64Sign dependsOn: - macOSARM64 @@ -337,7 +352,7 @@ stages: steps: - template: darwin/product-build-darwin-sign.yml - - ${{ if eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true) }}: + - ${{ if and(eq(variables['VSCODE_CIBUILD'], false), eq(variables['VSCODE_BUILD_MACOS_UNIVERSAL'], true)) }}: - job: macOSUniversal dependsOn: - macOS @@ -346,8 +361,8 @@ stages: variables: VSCODE_ARCH: universal steps: - - template: darwin/product-build-darwin.yml - - ${{ if ne(variables['VSCODE_PUBLISH'], 'false') }}: + - template: darwin/product-build-darwin-universal.yml + - ${{ if eq(variables['VSCODE_CIBUILD'], false) }}: - job: macOSUniversalSign dependsOn: - macOSUniversal diff --git a/build/azure-pipelines/product-publish.ps1 b/build/azure-pipelines/product-publish.ps1 index 17b02f6aff5..5abfed48dca 100644 --- a/build/azure-pipelines/product-publish.ps1 +++ b/build/azure-pipelines/product-publish.ps1 @@ -29,8 +29,8 @@ if (Test-Path $ARTIFACT_PROCESSED_WILDCARD_PATH) { # This means that the latest artifact_processed_*.txt file has all of the contents of the previous ones. # Note: The kusto-like syntax only works in PS7+ and only in scripts, not at the REPL. Get-ChildItem $ARTIFACT_PROCESSED_WILDCARD_PATH - | Sort-Object - | Select-Object -Last 1 + # Sort by file name length first and then Name to make sure we sort numerically. Ex. 12 comes after 9. + | Sort-Object { $_.Name.Length },Name -Bottom 1 | Get-Content | ForEach-Object { $set.Add($_) | Out-Null diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 79ef705a07b..99b39ee2b25 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -282,7 +282,7 @@ steps: targetPath: .build\logs displayName: "Publish Log Files" continueOnError: true - condition: and(succeededOrFailed(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) + condition: and(failed(), eq(variables['VSCODE_STEP_ON_IT'], 'false'), ne(variables['VSCODE_ARCH'], 'arm64')) - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/build/filters.js b/build/filters.js index f462566d0f4..c2a0a52bdb9 100644 --- a/build/filters.js +++ b/build/filters.js @@ -112,7 +112,7 @@ module.exports.indentationFilter = [ '!src/vs/*/**/*.d.ts', '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', - '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus}', + '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml}', '!build/{lib,download,linux,darwin}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 2f5a931e850..e6c30fec111 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -50,7 +50,6 @@ const vscodeEntryPoints = _.flatten([ const vscodeResources = [ 'out-build/main.js', 'out-build/cli.js', - 'out-build/driver.js', 'out-build/bootstrap.js', 'out-build/bootstrap-fork.js', 'out-build/bootstrap-amd.js', @@ -331,6 +330,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); + + result = es.merge(result, gulp.src('resources/win32/policies/**', { base: 'resources/win32' })); } else if (platform === 'linux') { result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) .pipe(replace('@@PRODNAME@@', product.nameLong)) diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 3c6a7b3ee89..7d0f70f9bef 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -176,7 +176,7 @@ function prepareRpmPackage(arch) { const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) .pipe(rename(function (p) { p.dirname = 'BUILD/usr/share/' + product.applicationName + '/' + p.dirname; })); - const dependencies = rpmDependenciesGenerator.getDependencies(binaryDir, product.applicationName); + const dependencies = rpmDependenciesGenerator.getDependencies(binaryDir, product.applicationName, rpmArch); const spec = gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@NAME_LONG@@', product.nameLong)) diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index ea53c1b8c69..f0ad617f405 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -32,7 +32,7 @@ const version = (quality && quality !== 'stable') ? `${packageJson.version}-${qu const vscodeWebResourceIncludes = [ // Workbench - 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg}', + 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg,opus}', 'out-build/vs/code/browser/workbench/*.html', 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', 'out-build/vs/**/markdown.css', diff --git a/build/lib/electron.js b/build/lib/electron.js index f623e8cb1b0..362f6c38e69 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -37,7 +37,7 @@ const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSyn * If you call `darwinBundleDocumentType(..., 'bat', 'Windows command script')`, the file type is `"Windows command script"`, * and the `'bat'` darwin icon is used. */ -function darwinBundleDocumentType(extensions, icon, nameOrSuffix) { +function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { // If given a suffix, generate a name from it. If not given anything, default to 'document' if (isDocumentSuffix(nameOrSuffix) || !nameOrSuffix) { nameOrSuffix = icon.charAt(0).toUpperCase() + icon.slice(1) + ' ' + (nameOrSuffix ?? 'document'); @@ -46,8 +46,9 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix) { name: nameOrSuffix, role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], - extensions: extensions, - iconFile: 'resources/darwin/' + icon + '.icns' + extensions, + iconFile: 'resources/darwin/' + icon + '.icns', + utis }; } /** @@ -65,11 +66,11 @@ function darwinBundleDocumentTypes(types, icon) { return Object.keys(types).map((name) => { const extensions = types[name]; return { - name: name, + name, role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions: Array.isArray(extensions) ? extensions : [extensions], - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon + '.icns' }; }); } @@ -156,7 +157,9 @@ exports.config = { darwinBundleDocumentType([ 'containerfile', 'ctp', 'dot', 'edn', 'handlebars', 'hbs', 'ml', 'mli', 'pl', 'pl6', 'pm', 'pm6', 'pod', 'pp', 'properties', 'psgi', 'rt', 't' - ], 'default', product.nameLong + ' document') + ], 'default', product.nameLong + ' document'), + // Folder support () + darwinBundleDocumentType([], 'default', 'Folder', ['public.folder']) ], darwinBundleURLTypes: [{ role: 'Viewer', diff --git a/build/lib/electron.ts b/build/lib/electron.ts index a3d696b75cc..ed91f8c2f71 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -19,6 +19,7 @@ type DarwinDocumentType = { ostypes: string[]; extensions: string[]; iconFile: string; + utis?: string[]; }; function isDocumentSuffix(str?: string): str is DarwinDocumentSuffix { @@ -50,7 +51,7 @@ const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSyn * If you call `darwinBundleDocumentType(..., 'bat', 'Windows command script')`, the file type is `"Windows command script"`, * and the `'bat'` darwin icon is used. */ -function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuffix?: string | DarwinDocumentSuffix): DarwinDocumentType { +function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuffix?: string | DarwinDocumentSuffix, utis?: string[]): DarwinDocumentType { // If given a suffix, generate a name from it. If not given anything, default to 'document' if (isDocumentSuffix(nameOrSuffix) || !nameOrSuffix) { nameOrSuffix = icon.charAt(0).toUpperCase() + icon.slice(1) + ' ' + (nameOrSuffix ?? 'document'); @@ -60,8 +61,9 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff name: nameOrSuffix, role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], - extensions: extensions, - iconFile: 'resources/darwin/' + icon + '.icns' + extensions, + iconFile: 'resources/darwin/' + icon + '.icns', + utis }; } @@ -80,11 +82,11 @@ function darwinBundleDocumentTypes(types: { [name: string]: string | string[] }, return Object.keys(types).map((name: string): DarwinDocumentType => { const extensions = types[name]; return { - name: name, + name, role: 'Editor', ostypes: ['TEXT', 'utxt', 'TUTX', '****'], extensions: Array.isArray(extensions) ? extensions : [extensions], - iconFile: 'resources/darwin/' + icon + '.icns', + iconFile: 'resources/darwin/' + icon + '.icns' } as DarwinDocumentType; }); } @@ -172,7 +174,9 @@ export const config = { darwinBundleDocumentType([ 'containerfile', 'ctp', 'dot', 'edn', 'handlebars', 'hbs', 'ml', 'mli', 'pl', 'pl6', 'pm', 'pm6', 'pod', 'pp', 'properties', 'psgi', 'rt', 't' - ], 'default', product.nameLong + ' document') + ], 'default', product.nameLong + ' document'), + // Folder support () + darwinBundleDocumentType([], 'default', 'Folder', ['public.folder']) ], darwinBundleURLTypes: [{ role: 'Viewer', diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 00f153acfc8..7c980a43a44 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -40,7 +40,7 @@ function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.N return result; } -function clone(object: T): T { +function clone(object: T): T { const result = {}; for (const id in object) { result[id] = object[id]; diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js index f72b442670a..99064a1f7e6 100644 --- a/build/linux/rpm/dep-lists.js +++ b/build/linux/rpm/dep-lists.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.bundledDeps = exports.additionalDeps = void 0; +exports.referenceGeneratedDepsByArch = exports.bundledDeps = exports.additionalDeps = void 0; // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/additional_deps // Additional dependencies not in the rpm find-requires output. exports.additionalDeps = [ @@ -29,3 +29,281 @@ exports.bundledDeps = [ 'libvk_swiftshader.so', 'libffmpeg.so' ]; +exports.referenceGeneratedDepsByArch = { + 'x86_64': [ + 'ca-certificates', + 'ld-linux-x86-64.so.2()(64bit)', + 'ld-linux-x86-64.so.2(GLIBC_2.2.5)(64bit)', + 'ld-linux-x86-64.so.2(GLIBC_2.3)(64bit)', + 'libX11.so.6()(64bit)', + 'libXcomposite.so.1()(64bit)', + 'libXdamage.so.1()(64bit)', + 'libXext.so.6()(64bit)', + 'libXfixes.so.3()(64bit)', + 'libXrandr.so.2()(64bit)', + 'libasound.so.2()(64bit)', + 'libasound.so.2(ALSA_0.9)(64bit)', + 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', + 'libatk-1.0.so.0()(64bit)', + 'libatk-bridge-2.0.so.0()(64bit)', + 'libatspi.so.0()(64bit)', + 'libc.so.6()(64bit)', + 'libc.so.6(GLIBC_2.10)(64bit)', + 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.14)(64bit)', + 'libc.so.6(GLIBC_2.15)(64bit)', + 'libc.so.6(GLIBC_2.16)(64bit)', + 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.3)(64bit)', + 'libc.so.6(GLIBC_2.3.2)(64bit)', + 'libc.so.6(GLIBC_2.3.3)(64bit)', + 'libc.so.6(GLIBC_2.3.4)(64bit)', + 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.6)(64bit)', + 'libc.so.6(GLIBC_2.7)(64bit)', + 'libc.so.6(GLIBC_2.8)(64bit)', + 'libc.so.6(GLIBC_2.9)(64bit)', + 'libcairo.so.2()(64bit)', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3()(64bit)', + 'libdl.so.2()(64bit)', + 'libdl.so.2(GLIBC_2.2.5)(64bit)', + 'libdrm.so.2()(64bit)', + 'libexpat.so.1()(64bit)', + 'libgbm.so.1()(64bit)', + 'libgcc_s.so.1()(64bit)', + 'libgcc_s.so.1(GCC_3.0)(64bit)', + 'libgdk_pixbuf-2.0.so.0()(64bit)', + 'libgio-2.0.so.0()(64bit)', + 'libglib-2.0.so.0()(64bit)', + 'libgmodule-2.0.so.0()(64bit)', + 'libgobject-2.0.so.0()(64bit)', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6()(64bit)', + 'libm.so.6(GLIBC_2.2.5)(64bit)', + 'libnspr4.so()(64bit)', + 'libnss3.so()(64bit)', + 'libnss3.so(NSS_3.11)(64bit)', + 'libnss3.so(NSS_3.12)(64bit)', + 'libnss3.so(NSS_3.12.1)(64bit)', + 'libnss3.so(NSS_3.2)(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)(64bit)', + 'libnss3.so(NSS_3.4)(64bit)', + 'libnss3.so(NSS_3.5)(64bit)', + 'libnss3.so(NSS_3.9.2)(64bit)', + 'libnssutil3.so()(64bit)', + 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', + 'libpango-1.0.so.0()(64bit)', + 'libpthread.so.0()(64bit)', + 'libpthread.so.0(GLIBC_2.12)(64bit)', + 'libpthread.so.0(GLIBC_2.2.5)(64bit)', + 'libpthread.so.0(GLIBC_2.3.2)(64bit)', + 'libpthread.so.0(GLIBC_2.3.3)(64bit)', + 'libpthread.so.0(GLIBC_2.3.4)(64bit)', + 'librt.so.1()(64bit)', + 'librt.so.1(GLIBC_2.2.5)(64bit)', + 'libsecret-1.so.0()(64bit)', + 'libsmime3.so()(64bit)', + 'libsmime3.so(NSS_3.10)(64bit)', + 'libsmime3.so(NSS_3.2)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libutil.so.1()(64bit)', + 'libutil.so.1(GLIBC_2.2.5)(64bit)', + 'libxcb.so.1()(64bit)', + 'libxkbcommon.so.0()(64bit)', + 'libxkbfile.so.1()(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ], + 'armv7hl': [ + 'ca-certificates', + 'ld-linux-armhf.so.3', + 'ld-linux-armhf.so.3(GLIBC_2.4)', + 'libX11.so.6', + 'libXcomposite.so.1', + 'libXdamage.so.1', + 'libXext.so.6', + 'libXfixes.so.3', + 'libXrandr.so.2', + 'libasound.so.2', + 'libasound.so.2(ALSA_0.9)', + 'libasound.so.2(ALSA_0.9.0rc4)', + 'libatk-1.0.so.0', + 'libatk-bridge-2.0.so.0', + 'libatspi.so.0', + 'libc.so.6', + 'libc.so.6(GLIBC_2.10)', + 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.14)', + 'libc.so.6(GLIBC_2.15)', + 'libc.so.6(GLIBC_2.16)', + 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.6)', + 'libc.so.6(GLIBC_2.7)', + 'libc.so.6(GLIBC_2.8)', + 'libc.so.6(GLIBC_2.9)', + 'libcairo.so.2', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3', + 'libdl.so.2', + 'libdl.so.2(GLIBC_2.4)', + 'libdrm.so.2', + 'libexpat.so.1', + 'libgbm.so.1', + 'libgcc_s.so.1', + 'libgcc_s.so.1(GCC_3.0)', + 'libgcc_s.so.1(GCC_3.4)', + 'libgcc_s.so.1(GCC_3.5)', + 'libgdk_pixbuf-2.0.so.0', + 'libgio-2.0.so.0', + 'libglib-2.0.so.0', + 'libgmodule-2.0.so.0', + 'libgobject-2.0.so.0', + 'libgtk-3.so.0', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6', + 'libm.so.6(GLIBC_2.4)', + 'libnspr4.so', + 'libnss3.so', + 'libnss3.so(NSS_3.11)', + 'libnss3.so(NSS_3.12)', + 'libnss3.so(NSS_3.12.1)', + 'libnss3.so(NSS_3.2)', + 'libnss3.so(NSS_3.22)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)', + 'libnss3.so(NSS_3.4)', + 'libnss3.so(NSS_3.5)', + 'libnss3.so(NSS_3.9.2)', + 'libnssutil3.so', + 'libnssutil3.so(NSSUTIL_3.12.3)', + 'libpango-1.0.so.0', + 'libpthread.so.0', + 'libpthread.so.0(GLIBC_2.12)', + 'libpthread.so.0(GLIBC_2.4)', + 'librt.so.1', + 'librt.so.1(GLIBC_2.4)', + 'libsecret-1.so.0', + 'libsmime3.so', + 'libsmime3.so(NSS_3.10)', + 'libsmime3.so(NSS_3.2)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libstdc++.so.6', + 'libstdc++.so.6(CXXABI_1.3)', + 'libstdc++.so.6(CXXABI_1.3.5)', + 'libstdc++.so.6(CXXABI_1.3.8)', + 'libstdc++.so.6(CXXABI_1.3.9)', + 'libstdc++.so.6(CXXABI_ARM_1.3.3)', + 'libstdc++.so.6(GLIBCXX_3.4)', + 'libstdc++.so.6(GLIBCXX_3.4.11)', + 'libstdc++.so.6(GLIBCXX_3.4.14)', + 'libstdc++.so.6(GLIBCXX_3.4.15)', + 'libstdc++.so.6(GLIBCXX_3.4.18)', + 'libstdc++.so.6(GLIBCXX_3.4.19)', + 'libstdc++.so.6(GLIBCXX_3.4.20)', + 'libstdc++.so.6(GLIBCXX_3.4.21)', + 'libstdc++.so.6(GLIBCXX_3.4.22)', + 'libstdc++.so.6(GLIBCXX_3.4.5)', + 'libstdc++.so.6(GLIBCXX_3.4.9)', + 'libutil.so.1', + 'libutil.so.1(GLIBC_2.4)', + 'libxcb.so.1', + 'libxkbcommon.so.0', + 'libxkbfile.so.1', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ], + 'aarch64': [ + 'ca-certificates', + 'ld-linux-aarch64.so.1()(64bit)', + 'ld-linux-aarch64.so.1(GLIBC_2.17)(64bit)', + 'libX11.so.6()(64bit)', + 'libXcomposite.so.1()(64bit)', + 'libXdamage.so.1()(64bit)', + 'libXext.so.6()(64bit)', + 'libXfixes.so.3()(64bit)', + 'libXrandr.so.2()(64bit)', + 'libasound.so.2()(64bit)', + 'libasound.so.2(ALSA_0.9)(64bit)', + 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', + 'libatk-1.0.so.0()(64bit)', + 'libatk-bridge-2.0.so.0()(64bit)', + 'libatspi.so.0()(64bit)', + 'libc.so.6()(64bit)', + 'libc.so.6(GLIBC_2.17)(64bit)', + 'libcairo.so.2()(64bit)', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3()(64bit)', + 'libdbus-1.so.3(LIBDBUS_1_3)(64bit)', + 'libdl.so.2()(64bit)', + 'libdl.so.2(GLIBC_2.17)(64bit)', + 'libdrm.so.2()(64bit)', + 'libexpat.so.1()(64bit)', + 'libgbm.so.1()(64bit)', + 'libgcc_s.so.1()(64bit)', + 'libgcc_s.so.1(GCC_3.0)(64bit)', + 'libgcc_s.so.1(GCC_4.2.0)(64bit)', + 'libgcc_s.so.1(GCC_4.5.0)(64bit)', + 'libgdk_pixbuf-2.0.so.0()(64bit)', + 'libgio-2.0.so.0()(64bit)', + 'libglib-2.0.so.0()(64bit)', + 'libgmodule-2.0.so.0()(64bit)', + 'libgobject-2.0.so.0()(64bit)', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6()(64bit)', + 'libm.so.6(GLIBC_2.17)(64bit)', + 'libnspr4.so()(64bit)', + 'libnss3.so()(64bit)', + 'libnss3.so(NSS_3.11)(64bit)', + 'libnss3.so(NSS_3.12)(64bit)', + 'libnss3.so(NSS_3.12.1)(64bit)', + 'libnss3.so(NSS_3.2)(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)(64bit)', + 'libnss3.so(NSS_3.4)(64bit)', + 'libnss3.so(NSS_3.5)(64bit)', + 'libnss3.so(NSS_3.9.2)(64bit)', + 'libnssutil3.so()(64bit)', + 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', + 'libpango-1.0.so.0()(64bit)', + 'libpthread.so.0()(64bit)', + 'libpthread.so.0(GLIBC_2.17)(64bit)', + 'librt.so.1()(64bit)', + 'librt.so.1(GLIBC_2.17)(64bit)', + 'libsecret-1.so.0()(64bit)', + 'libsmime3.so()(64bit)', + 'libsmime3.so(NSS_3.10)(64bit)', + 'libsmime3.so(NSS_3.2)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libstdc++.so.6()(64bit)', + 'libstdc++.so.6(CXXABI_1.3)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.5)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.8)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.9)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.11)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.14)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.15)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.18)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.19)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.20)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', + 'libutil.so.1()(64bit)', + 'libutil.so.1(GLIBC_2.17)(64bit)', + 'libxcb.so.1()(64bit)', + 'libxkbcommon.so.0()(64bit)', + 'libxkbcommon.so.0(V_0.5.0)(64bit)', + 'libxkbfile.so.1()(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ] +}; diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 884f6825fbd..524cc0d7edd 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -28,3 +28,282 @@ export const bundledDeps = [ 'libvk_swiftshader.so', 'libffmpeg.so' ]; + +export const referenceGeneratedDepsByArch = { + 'x86_64': [ + 'ca-certificates', + 'ld-linux-x86-64.so.2()(64bit)', + 'ld-linux-x86-64.so.2(GLIBC_2.2.5)(64bit)', + 'ld-linux-x86-64.so.2(GLIBC_2.3)(64bit)', + 'libX11.so.6()(64bit)', + 'libXcomposite.so.1()(64bit)', + 'libXdamage.so.1()(64bit)', + 'libXext.so.6()(64bit)', + 'libXfixes.so.3()(64bit)', + 'libXrandr.so.2()(64bit)', + 'libasound.so.2()(64bit)', + 'libasound.so.2(ALSA_0.9)(64bit)', + 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', + 'libatk-1.0.so.0()(64bit)', + 'libatk-bridge-2.0.so.0()(64bit)', + 'libatspi.so.0()(64bit)', + 'libc.so.6()(64bit)', + 'libc.so.6(GLIBC_2.10)(64bit)', + 'libc.so.6(GLIBC_2.11)(64bit)', + 'libc.so.6(GLIBC_2.14)(64bit)', + 'libc.so.6(GLIBC_2.15)(64bit)', + 'libc.so.6(GLIBC_2.16)(64bit)', + 'libc.so.6(GLIBC_2.17)(64bit)', + 'libc.so.6(GLIBC_2.2.5)(64bit)', + 'libc.so.6(GLIBC_2.3)(64bit)', + 'libc.so.6(GLIBC_2.3.2)(64bit)', + 'libc.so.6(GLIBC_2.3.3)(64bit)', + 'libc.so.6(GLIBC_2.3.4)(64bit)', + 'libc.so.6(GLIBC_2.4)(64bit)', + 'libc.so.6(GLIBC_2.6)(64bit)', + 'libc.so.6(GLIBC_2.7)(64bit)', + 'libc.so.6(GLIBC_2.8)(64bit)', + 'libc.so.6(GLIBC_2.9)(64bit)', + 'libcairo.so.2()(64bit)', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3()(64bit)', + 'libdl.so.2()(64bit)', + 'libdl.so.2(GLIBC_2.2.5)(64bit)', + 'libdrm.so.2()(64bit)', + 'libexpat.so.1()(64bit)', + 'libgbm.so.1()(64bit)', + 'libgcc_s.so.1()(64bit)', + 'libgcc_s.so.1(GCC_3.0)(64bit)', + 'libgdk_pixbuf-2.0.so.0()(64bit)', + 'libgio-2.0.so.0()(64bit)', + 'libglib-2.0.so.0()(64bit)', + 'libgmodule-2.0.so.0()(64bit)', + 'libgobject-2.0.so.0()(64bit)', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6()(64bit)', + 'libm.so.6(GLIBC_2.2.5)(64bit)', + 'libnspr4.so()(64bit)', + 'libnss3.so()(64bit)', + 'libnss3.so(NSS_3.11)(64bit)', + 'libnss3.so(NSS_3.12)(64bit)', + 'libnss3.so(NSS_3.12.1)(64bit)', + 'libnss3.so(NSS_3.2)(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)(64bit)', + 'libnss3.so(NSS_3.4)(64bit)', + 'libnss3.so(NSS_3.5)(64bit)', + 'libnss3.so(NSS_3.9.2)(64bit)', + 'libnssutil3.so()(64bit)', + 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', + 'libpango-1.0.so.0()(64bit)', + 'libpthread.so.0()(64bit)', + 'libpthread.so.0(GLIBC_2.12)(64bit)', + 'libpthread.so.0(GLIBC_2.2.5)(64bit)', + 'libpthread.so.0(GLIBC_2.3.2)(64bit)', + 'libpthread.so.0(GLIBC_2.3.3)(64bit)', + 'libpthread.so.0(GLIBC_2.3.4)(64bit)', + 'librt.so.1()(64bit)', + 'librt.so.1(GLIBC_2.2.5)(64bit)', + 'libsecret-1.so.0()(64bit)', + 'libsmime3.so()(64bit)', + 'libsmime3.so(NSS_3.10)(64bit)', + 'libsmime3.so(NSS_3.2)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libutil.so.1()(64bit)', + 'libutil.so.1(GLIBC_2.2.5)(64bit)', + 'libxcb.so.1()(64bit)', + 'libxkbcommon.so.0()(64bit)', + 'libxkbfile.so.1()(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ], + 'armv7hl': [ + 'ca-certificates', + 'ld-linux-armhf.so.3', + 'ld-linux-armhf.so.3(GLIBC_2.4)', + 'libX11.so.6', + 'libXcomposite.so.1', + 'libXdamage.so.1', + 'libXext.so.6', + 'libXfixes.so.3', + 'libXrandr.so.2', + 'libasound.so.2', + 'libasound.so.2(ALSA_0.9)', + 'libasound.so.2(ALSA_0.9.0rc4)', + 'libatk-1.0.so.0', + 'libatk-bridge-2.0.so.0', + 'libatspi.so.0', + 'libc.so.6', + 'libc.so.6(GLIBC_2.10)', + 'libc.so.6(GLIBC_2.11)', + 'libc.so.6(GLIBC_2.14)', + 'libc.so.6(GLIBC_2.15)', + 'libc.so.6(GLIBC_2.16)', + 'libc.so.6(GLIBC_2.17)', + 'libc.so.6(GLIBC_2.4)', + 'libc.so.6(GLIBC_2.6)', + 'libc.so.6(GLIBC_2.7)', + 'libc.so.6(GLIBC_2.8)', + 'libc.so.6(GLIBC_2.9)', + 'libcairo.so.2', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3', + 'libdl.so.2', + 'libdl.so.2(GLIBC_2.4)', + 'libdrm.so.2', + 'libexpat.so.1', + 'libgbm.so.1', + 'libgcc_s.so.1', + 'libgcc_s.so.1(GCC_3.0)', + 'libgcc_s.so.1(GCC_3.4)', + 'libgcc_s.so.1(GCC_3.5)', + 'libgdk_pixbuf-2.0.so.0', + 'libgio-2.0.so.0', + 'libglib-2.0.so.0', + 'libgmodule-2.0.so.0', + 'libgobject-2.0.so.0', + 'libgtk-3.so.0', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6', + 'libm.so.6(GLIBC_2.4)', + 'libnspr4.so', + 'libnss3.so', + 'libnss3.so(NSS_3.11)', + 'libnss3.so(NSS_3.12)', + 'libnss3.so(NSS_3.12.1)', + 'libnss3.so(NSS_3.2)', + 'libnss3.so(NSS_3.22)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)', + 'libnss3.so(NSS_3.4)', + 'libnss3.so(NSS_3.5)', + 'libnss3.so(NSS_3.9.2)', + 'libnssutil3.so', + 'libnssutil3.so(NSSUTIL_3.12.3)', + 'libpango-1.0.so.0', + 'libpthread.so.0', + 'libpthread.so.0(GLIBC_2.12)', + 'libpthread.so.0(GLIBC_2.4)', + 'librt.so.1', + 'librt.so.1(GLIBC_2.4)', + 'libsecret-1.so.0', + 'libsmime3.so', + 'libsmime3.so(NSS_3.10)', + 'libsmime3.so(NSS_3.2)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libstdc++.so.6', + 'libstdc++.so.6(CXXABI_1.3)', + 'libstdc++.so.6(CXXABI_1.3.5)', + 'libstdc++.so.6(CXXABI_1.3.8)', + 'libstdc++.so.6(CXXABI_1.3.9)', + 'libstdc++.so.6(CXXABI_ARM_1.3.3)', + 'libstdc++.so.6(GLIBCXX_3.4)', + 'libstdc++.so.6(GLIBCXX_3.4.11)', + 'libstdc++.so.6(GLIBCXX_3.4.14)', + 'libstdc++.so.6(GLIBCXX_3.4.15)', + 'libstdc++.so.6(GLIBCXX_3.4.18)', + 'libstdc++.so.6(GLIBCXX_3.4.19)', + 'libstdc++.so.6(GLIBCXX_3.4.20)', + 'libstdc++.so.6(GLIBCXX_3.4.21)', + 'libstdc++.so.6(GLIBCXX_3.4.22)', + 'libstdc++.so.6(GLIBCXX_3.4.5)', + 'libstdc++.so.6(GLIBCXX_3.4.9)', + 'libutil.so.1', + 'libutil.so.1(GLIBC_2.4)', + 'libxcb.so.1', + 'libxkbcommon.so.0', + 'libxkbfile.so.1', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ], + 'aarch64': [ + 'ca-certificates', + 'ld-linux-aarch64.so.1()(64bit)', + 'ld-linux-aarch64.so.1(GLIBC_2.17)(64bit)', + 'libX11.so.6()(64bit)', + 'libXcomposite.so.1()(64bit)', + 'libXdamage.so.1()(64bit)', + 'libXext.so.6()(64bit)', + 'libXfixes.so.3()(64bit)', + 'libXrandr.so.2()(64bit)', + 'libasound.so.2()(64bit)', + 'libasound.so.2(ALSA_0.9)(64bit)', + 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', + 'libatk-1.0.so.0()(64bit)', + 'libatk-bridge-2.0.so.0()(64bit)', + 'libatspi.so.0()(64bit)', + 'libc.so.6()(64bit)', + 'libc.so.6(GLIBC_2.17)(64bit)', + 'libcairo.so.2()(64bit)', + 'libcurl.so.4()(64bit)', + 'libdbus-1.so.3()(64bit)', + 'libdbus-1.so.3(LIBDBUS_1_3)(64bit)', + 'libdl.so.2()(64bit)', + 'libdl.so.2(GLIBC_2.17)(64bit)', + 'libdrm.so.2()(64bit)', + 'libexpat.so.1()(64bit)', + 'libgbm.so.1()(64bit)', + 'libgcc_s.so.1()(64bit)', + 'libgcc_s.so.1(GCC_3.0)(64bit)', + 'libgcc_s.so.1(GCC_4.2.0)(64bit)', + 'libgcc_s.so.1(GCC_4.5.0)(64bit)', + 'libgdk_pixbuf-2.0.so.0()(64bit)', + 'libgio-2.0.so.0()(64bit)', + 'libglib-2.0.so.0()(64bit)', + 'libgmodule-2.0.so.0()(64bit)', + 'libgobject-2.0.so.0()(64bit)', + 'libgtk-3.so.0()(64bit)', + 'libm.so.6()(64bit)', + 'libm.so.6(GLIBC_2.17)(64bit)', + 'libnspr4.so()(64bit)', + 'libnss3.so()(64bit)', + 'libnss3.so(NSS_3.11)(64bit)', + 'libnss3.so(NSS_3.12)(64bit)', + 'libnss3.so(NSS_3.12.1)(64bit)', + 'libnss3.so(NSS_3.2)(64bit)', + 'libnss3.so(NSS_3.22)(64bit)', + 'libnss3.so(NSS_3.3)(64bit)', + 'libnss3.so(NSS_3.4)(64bit)', + 'libnss3.so(NSS_3.5)(64bit)', + 'libnss3.so(NSS_3.9.2)(64bit)', + 'libnssutil3.so()(64bit)', + 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', + 'libpango-1.0.so.0()(64bit)', + 'libpthread.so.0()(64bit)', + 'libpthread.so.0(GLIBC_2.17)(64bit)', + 'librt.so.1()(64bit)', + 'librt.so.1(GLIBC_2.17)(64bit)', + 'libsecret-1.so.0()(64bit)', + 'libsmime3.so()(64bit)', + 'libsmime3.so(NSS_3.10)(64bit)', + 'libsmime3.so(NSS_3.2)(64bit)', + 'libssl3.so(NSS_3.28)(64bit)', + 'libstdc++.so.6()(64bit)', + 'libstdc++.so.6(CXXABI_1.3)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.5)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.8)(64bit)', + 'libstdc++.so.6(CXXABI_1.3.9)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.11)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.14)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.15)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.18)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.19)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.20)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', + 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', + 'libutil.so.1()(64bit)', + 'libutil.so.1(GLIBC_2.17)(64bit)', + 'libxcb.so.1()(64bit)', + 'libxkbcommon.so.0()(64bit)', + 'libxkbcommon.so.0(V_0.5.0)(64bit)', + 'libxkbfile.so.1()(64bit)', + 'rpmlib(FileDigests) <= 4.6.0-1', + 'rtld(GNU_HASH)', + 'xdg-utils' + ] +}; diff --git a/build/linux/rpm/dependencies-generator.js b/build/linux/rpm/dependencies-generator.js index ddeefe3c28d..1d91eb8de78 100644 --- a/build/linux/rpm/dependencies-generator.js +++ b/build/linux/rpm/dependencies-generator.js @@ -9,7 +9,15 @@ const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path = require("path"); const dep_lists_1 = require("./dep-lists"); -function getDependencies(buildDir, applicationName) { +// A flag that can easily be toggled. +// Make sure to compile the build directory after toggling the value. +// If false, we warn about new dependencies if they show up +// while running the rpm prepare package task for a release. +// If true, we fail the build if there are new dependencies found during that task. +// The reference dependencies, which one has to update when the new dependencies +// are valid, are in dep-lists.ts +const FAIL_BUILD_FOR_NEW_DEPENDENCIES = true; +function getDependencies(buildDir, applicationName, arch) { // Get the files for which we want to find dependencies. const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); const findResult = (0, child_process_1.spawnSync)('find', [nativeModulesPath, '-name', '*.node']); @@ -40,9 +48,22 @@ function getDependencies(buildDir, applicationName) { sortedDependencies = sortedDependencies.filter(dependency => { return !dep_lists_1.bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); }); + const referenceGeneratedDeps = dep_lists_1.referenceGeneratedDepsByArch[arch]; + if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) { + const failMessage = 'The dependencies list has changed. ' + + 'Printing newer dependencies list that one can use to compare against referenceGeneratedDeps:\n' + + sortedDependencies.join('\n'); + if (FAIL_BUILD_FOR_NEW_DEPENDENCIES) { + throw new Error(failMessage); + } + else { + console.warn(failMessage); + } + } return sortedDependencies; } exports.getDependencies = getDependencies; +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/calculate_package_deps.py. function calculatePackageDeps(binaryPath) { try { if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) { @@ -58,9 +79,6 @@ function calculatePackageDeps(binaryPath) { throw new Error(`find-requires failed with exit code ${findRequiresResult.status}.\nstderr: ${findRequiresResult.stderr}`); } const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); - // we only need to use provides to check for newer dependencies - // const provides = readFileSync('dist_package_provides.json'); - // const jsonProvides = JSON.parse(provides.toString('utf-8')); return requires; } // Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py diff --git a/build/linux/rpm/dependencies-generator.ts b/build/linux/rpm/dependencies-generator.ts index 97d313fe570..95953ecaa24 100644 --- a/build/linux/rpm/dependencies-generator.ts +++ b/build/linux/rpm/dependencies-generator.ts @@ -8,9 +8,19 @@ import { spawnSync } from 'child_process'; import { constants, statSync } from 'fs'; import path = require('path'); -import { additionalDeps, bundledDeps } from './dep-lists'; +import { additionalDeps, bundledDeps, referenceGeneratedDepsByArch } from './dep-lists'; +import { ArchString } from './types'; -export function getDependencies(buildDir: string, applicationName: string): string[] { +// A flag that can easily be toggled. +// Make sure to compile the build directory after toggling the value. +// If false, we warn about new dependencies if they show up +// while running the rpm prepare package task for a release. +// If true, we fail the build if there are new dependencies found during that task. +// The reference dependencies, which one has to update when the new dependencies +// are valid, are in dep-lists.ts +const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; + +export function getDependencies(buildDir: string, applicationName: string, arch: ArchString): string[] { // Get the files for which we want to find dependencies. const nativeModulesPath = path.join(buildDir, 'resources', 'app', 'node_modules.asar.unpacked'); const findResult = spawnSync('find', [nativeModulesPath, '-name', '*.node']); @@ -49,9 +59,22 @@ export function getDependencies(buildDir: string, applicationName: string): stri return !bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); }); + const referenceGeneratedDeps = referenceGeneratedDepsByArch[arch]; + if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) { + const failMessage = 'The dependencies list has changed. ' + + 'Printing newer dependencies list that one can use to compare against referenceGeneratedDeps:\n' + + sortedDependencies.join('\n'); + if (FAIL_BUILD_FOR_NEW_DEPENDENCIES) { + throw new Error(failMessage); + } else { + console.warn(failMessage); + } + } + return sortedDependencies; } +// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/calculate_package_deps.py. function calculatePackageDeps(binaryPath: string): Set { try { if (!(statSync(binaryPath).mode & constants.S_IXUSR)) { @@ -68,11 +91,6 @@ function calculatePackageDeps(binaryPath: string): Set { } const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); - - // we only need to use provides to check for newer dependencies - // const provides = readFileSync('dist_package_provides.json'); - // const jsonProvides = JSON.parse(provides.toString('utf-8')); - return requires; } diff --git a/build/linux/rpm/types.js b/build/linux/rpm/types.js new file mode 100644 index 00000000000..56d4e6c56ce --- /dev/null +++ b/build/linux/rpm/types.js @@ -0,0 +1,6 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/build/linux/rpm/types.ts b/build/linux/rpm/types.ts new file mode 100644 index 00000000000..84330949d1d --- /dev/null +++ b/build/linux/rpm/types.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export type ArchString = 'x86_64' | 'armv7hl' | 'aarch64'; diff --git a/build/package.json b/build/package.json index 3403f5c5c13..ab6b24d496d 100644 --- a/build/package.json +++ b/build/package.json @@ -54,6 +54,7 @@ "fs-extra": "^9.1.0", "got": "11.8.1", "gulp-merge-json": "^2.1.1", + "gulp-shell": "^0.8.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", "mkdirp": "^1.0.4", diff --git a/build/win32/code.iss b/build/win32/code.iss index 87367f14907..e96ca4bde77 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -82,6 +82,9 @@ Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent +[Dirs] +Name: "{app}"; AfterInstall: DisableAppDirInheritance + [Files] Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\resources\app\product.json"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion @@ -1480,3 +1483,19 @@ end; #ifdef Debug #expr SaveToFile(AddBackslash(SourcePath) + "code-processed.iss") #endif + +// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/icacls +// https://docs.microsoft.com/en-US/windows/security/identity-protection/access-control/security-identifiers +procedure DisableAppDirInheritance(); +var + ResultCode: Integer; + Permissions: string; +begin + Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"'; + + #if "user" == InstallTarget + Permissions := Permissions + ' /grant:r "*S-1-3-0:(OI)(CI)F"'; + #endif + + Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; \ No newline at end of file diff --git a/build/yarn.lock b/build/yarn.lock index 9b91f75047d..db78aeacc16 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -798,6 +798,13 @@ ansi-colors@^1.0.1: dependencies: ansi-wrap "^0.1.0" +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -805,7 +812,14 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-wrap@^0.1.0: +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= @@ -1033,6 +1047,14 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + cheerio-select@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" @@ -1109,11 +1131,28 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + colors@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -1592,6 +1631,16 @@ extract-zip@^2.0.1: optionalDependencies: "@types/yauzl" "^2.9.1" +fancy-log@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + fast-glob@^3.2.9: version "3.2.11" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" @@ -1832,11 +1881,28 @@ gulp-merge-json@^2.1.1: through "^2.3.8" vinyl "^2.1.0" +gulp-shell@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.8.0.tgz#0ed4980de1d0c67e5f6cce971d7201fd0be50555" + integrity sha512-wHNCgmqbWkk1c6Gc2dOL5SprcoeujQdeepICwfQRo91DIylTE7a794VEE+leq3cE2YDoiS5ulvRfKVIEMazcTQ== + dependencies: + chalk "^3.0.0" + fancy-log "^1.3.3" + lodash.template "^4.5.0" + plugin-error "^1.0.1" + through2 "^3.0.1" + tslib "^1.10.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" @@ -1909,7 +1975,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2115,6 +2181,11 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -2155,6 +2226,21 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash@^4.17.10, lodash@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -2380,6 +2466,11 @@ p-limit@*, p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + parse-semver@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/parse-semver/-/parse-semver-1.1.1.tgz#9a4afd6df063dc4826f93fba4a99cf223f666cb8" @@ -2533,7 +2624,7 @@ read@^1.0.7: dependencies: mute-stream "~0.0.4" -readable-stream@3: +"readable-stream@2 || 3", readable-stream@3: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -2772,6 +2863,21 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +through2@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.2.tgz#99f88931cfc761ec7678b41d5d7336b5b6a07bf4" + integrity sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ== + dependencies: + inherits "^2.0.4" + readable-stream "2 || 3" + through2@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" @@ -2784,6 +2890,11 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + tmp@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -2817,6 +2928,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= +tslib@^1.10.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^1.8.1: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" diff --git a/cgmanifest.json b/cgmanifest.json index 31fc792caf7..1d2e0f72b7b 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "0b031afcefa031c322843d5964e999768b95af0a" + "commitHash": "73c87bcfc6e18428c21676d68f829364e6a7b15d" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "17.4.0" + "version": "17.4.1" }, { "component": { diff --git a/extensions/git-base/src/api/git-base.d.ts b/extensions/git-base/src/api/git-base.d.ts index 70ac3b1b972..8510df6d043 100644 --- a/extensions/git-base/src/api/git-base.d.ts +++ b/extensions/git-base/src/api/git-base.d.ts @@ -31,9 +31,12 @@ export interface GitBaseExtension { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; - readonly urlLabel?: string; + readonly urlLabel?: string | ((url: string) => string); readonly providerName?: string; + readonly title?: string; + readonly placeholder?: string; readonly branch?: boolean; // then result is PickRemoteSourceResult + readonly showRecentSources?: boolean; } export interface PickRemoteSourceResult { @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult { export interface RemoteSource { readonly name: string; readonly description?: string; + readonly detail?: string; + /** + * Codicon name + */ + readonly icon?: string; readonly url: string | string[]; } +export interface RecentRemoteSource extends RemoteSource { + readonly timestamp: number; +} + export interface RemoteSourceProvider { readonly name: string; /** * Codicon name */ readonly icon?: string; + readonly label?: string; + readonly placeholder?: string; readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index 50dec70863e..83e83ae1fa9 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QuickPickItem, window, QuickPick } from 'vscode'; +import { QuickPickItem, window, QuickPick, QuickPickItemKind } from 'vscode'; import * as nls from 'vscode-nls'; import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base'; import { Model } from './model'; @@ -24,17 +24,20 @@ async function getQuickPickResult(quickpick: QuickPick< class RemoteSourceProviderQuickPick { - private quickpick: QuickPick; + private quickpick: QuickPick | undefined; - constructor(private provider: RemoteSourceProvider) { - this.quickpick = window.createQuickPick(); - this.quickpick.ignoreFocusOut = true; + constructor(private provider: RemoteSourceProvider) { } - if (provider.supportsQuery) { - this.quickpick.placeholder = localize('type to search', "Repository name (type to search)"); - this.quickpick.onDidChangeValue(this.onDidChangeValue, this); - } else { - this.quickpick.placeholder = localize('type to filter', "Repository name"); + private ensureQuickPick() { + if (!this.quickpick) { + this.quickpick = window.createQuickPick(); + this.quickpick.ignoreFocusOut = true; + if (this.provider.supportsQuery) { + this.quickpick.placeholder = this.provider.placeholder ?? localize('type to search', "Repository name (type to search)"); + this.quickpick.onDidChangeValue(this.onDidChangeValue, this); + } else { + this.quickpick.placeholder = this.provider.placeholder ?? localize('type to filter', "Repository name"); + } } } @@ -45,35 +48,37 @@ class RemoteSourceProviderQuickPick { @throttle private async query(): Promise { - this.quickpick.busy = true; - try { - const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || []; + const remoteSources = await this.provider.getRemoteSources(this.quickpick?.value) || []; + + this.ensureQuickPick(); + this.quickpick!.show(); if (remoteSources.length === 0) { - this.quickpick.items = [{ + this.quickpick!.items = [{ label: localize('none found', "No remote repositories found."), alwaysShow: true }]; } else { - this.quickpick.items = remoteSources.map(remoteSource => ({ - label: remoteSource.name, + this.quickpick!.items = remoteSources.map(remoteSource => ({ + label: remoteSource.icon ? `$(${remoteSource.icon}) ${remoteSource.name}` : remoteSource.name, description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]), + detail: remoteSource.detail, remoteSource, alwaysShow: true })); } } catch (err) { - this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; + this.quickpick!.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; console.error(err); } finally { - this.quickpick.busy = false; + this.quickpick!.busy = false; } } async pick(): Promise { - this.query(); - const result = await getQuickPickResult(this.quickpick); + await this.query(); + const result = await getQuickPickResult(this.quickpick!); return result?.remoteSource; } } @@ -83,6 +88,7 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider; url?: string })>(); quickpick.ignoreFocusOut = true; + quickpick.title = options.title; if (options.providerName) { const provider = model.getRemoteProviders() @@ -93,24 +99,47 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp } } - const providers = model.getRemoteProviders() + const remoteProviders = model.getRemoteProviders() .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider })); - quickpick.placeholder = providers.length === 0 + const recentSources: (QuickPickItem & { url?: string; timestamp: number })[] = []; + if (options.showRecentSources) { + for (const { provider } of remoteProviders) { + const sources = (await provider.getRecentRemoteSources?.() ?? []).map((item) => { + return { + ...item, + label: (item.icon ? `$(${item.icon}) ` : '') + item.name, + url: typeof item.url === 'string' ? item.url : item.url[0], + }; + }); + recentSources.push(...sources); + } + } + + const items = [ + { kind: QuickPickItemKind.Separator, label: localize('remote sources', 'remote sources') }, + ...remoteProviders, + { kind: QuickPickItemKind.Separator, label: localize('recently opened', 'recently opened') }, + ...recentSources.sort((a, b) => b.timestamp - a.timestamp) + ]; + + quickpick.placeholder = options.placeholder ?? (remoteProviders.length === 0 ? localize('provide url', "Provide repository URL") - : localize('provide url or pick', "Provide repository URL or pick a repository source."); + : localize('provide url or pick', "Provide repository URL or pick a repository source.")); const updatePicks = (value?: string) => { if (value) { + const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? localize('url', "URL"); quickpick.items = [{ - label: options.urlLabel ?? localize('url', "URL"), + label: label, description: value, alwaysShow: true, url: value }, - ...providers]; + ...items + ]; } else { - quickpick.items = providers; + quickpick.items = items; } }; diff --git a/extensions/git/package.json b/extensions/git/package.json index 1122b41fc4c..ab271e1aeb8 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -15,7 +15,6 @@ "scmActionButton", "scmSelectedProvider", "scmValidation", - "tabs", "timeline" ], "categories": [ @@ -2222,6 +2221,12 @@ "description": "%config.timeline.showAuthor%", "scope": "window" }, + "git.timeline.showUncommitted": { + "type": "boolean", + "default": false, + "description": "%config.timeline.showUncommitted%", + "scope": "window" + }, "git.showUnpublishedCommitsButton": { "type": "string", "enum": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6675bd12718..b19611f7dd9 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -188,6 +188,7 @@ "config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.", "config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.", "config.timeline.showAuthor": "Controls whether to show the commit author in the Timeline view.", + "config.timeline.showUncommitted": "Controls whether to show uncommitted changes in the Timeline view.", "config.timeline.date": "Controls which date to use for items in the Timeline view.", "config.timeline.date.committed": "Use the committed date", "config.timeline.date.authored": "Use the authored date", diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index e37e2dbd269..8fdab7f659b 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -396,7 +396,7 @@ export class Git { return Versions.compare(Versions.fromString(this.version), Versions.fromString(version)); } - open(repository: string, dotGit: string): Repository { + open(repository: string, dotGit: { path: string; commonPath?: string }): Repository { return new Repository(this, repository, dotGit); } @@ -509,15 +509,25 @@ export class Git { return repoPath; } - async getRepositoryDotGit(repositoryPath: string): Promise { - const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir']); - let dotGitPath = result.stdout.trim(); + async getRepositoryDotGit(repositoryPath: string): Promise<{ path: string; commonPath?: string }> { + const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir', '--git-common-dir']); + let [dotGitPath, commonDotGitPath] = result.stdout.split('\n').map(r => r.trim()); if (!path.isAbsolute(dotGitPath)) { dotGitPath = path.join(repositoryPath, dotGitPath); } + dotGitPath = path.normalize(dotGitPath); - return path.normalize(dotGitPath); + if (commonDotGitPath) { + if (!path.isAbsolute(commonDotGitPath)) { + commonDotGitPath = path.join(repositoryPath, commonDotGitPath); + } + commonDotGitPath = path.normalize(commonDotGitPath); + + return { path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined }; + } + + return { path: dotGitPath }; } async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise> { @@ -863,7 +873,7 @@ export class Repository { constructor( private _git: Git, private repositoryRoot: string, - readonly dotGit: string + readonly dotGit: { path: string; commonPath?: string } ) { } get git(): Git { diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index f0236dc3304..71ac937c6f8 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -105,6 +105,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR private _onDidRemoveRemoteSourcePublisher = new EventEmitter(); readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event; + private showRepoOnHomeDriveRootWarning = true; private pushErrorHandlers = new Set(); private disposables: Disposable[] = []; @@ -334,6 +335,22 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR return; } + // On Window, opening a git repository from the root of the HOMEDRIVE poses a security risk. + // We will only a open git repository from the root of the HOMEDRIVE if the user explicitly + // opens the HOMEDRIVE as a folder. Only show the warning once during repository discovery. + if (process.platform === 'win32' && process.env.HOMEDRIVE && pathEquals(`${process.env.HOMEDRIVE}\\`, repositoryRoot)) { + const isRepoInWorkspaceFolders = (workspace.workspaceFolders ?? []).find(f => pathEquals(f.uri.fsPath, repositoryRoot))!!; + + if (!isRepoInWorkspaceFolders) { + if (this.showRepoOnHomeDriveRootWarning) { + window.showWarningMessage(localize('repoOnHomeDriveRootWarning', "Unable to automatically open the git repository at '{0}'. To open that git repository, open it directly as a folder in VS Code.", repositoryRoot)); + this.showRepoOnHomeDriveRootWarning = false; + } + + return; + } + } + const dotGit = await this.git.getRepositoryDotGit(repositoryRoot); const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel, this.telemetryReporter); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 1472c3e32b8..76c30e5c1d4 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabKindTextDiff, TabKindNotebookDiff, RelativePattern } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git'; @@ -541,7 +541,7 @@ class DotGitWatcher implements IFileWatcher { private repository: Repository, private outputChannel: OutputChannel ) { - const rootWatcher = watch(repository.dotGit); + const rootWatcher = watch(repository.dotGit.path); this.disposables.push(rootWatcher); // Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files. @@ -563,7 +563,7 @@ class DotGitWatcher implements IFileWatcher { this.transientDisposables = dispose(this.transientDisposables); const { name, remote } = this.repository.HEAD.upstream; - const upstreamPath = path.join(this.repository.dotGit, 'refs', 'remotes', remote, name); + const upstreamPath = path.join(this.repository.dotGit.commonPath ?? this.repository.dotGit.path, 'refs', 'remotes', remote, name); try { const upstreamWatcher = watch(upstreamPath); @@ -842,7 +842,7 @@ export class Repository implements Disposable { return this.repository.root; } - get dotGit(): string { + get dotGit(): { path: string; commonPath?: string } { return this.repository.dotGit; } @@ -874,7 +874,7 @@ export class Repository implements Disposable { this.disposables.push(dotGitFileWatcher); } catch (err) { if (Log.logLevel <= LogLevel.Error) { - outputChannel.appendLine(`${logTimestamp()} Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); + outputChannel.appendLine(`${logTimestamp()} Failed to watch path:'${this.dotGit.path}' or commonPath:'${this.dotGit.commonPath}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); } onRepositoryDotGitFileChange = filterEvent(onRepositoryFileChange, uri => /\.git($|\/)/.test(uri.path)); @@ -1274,13 +1274,13 @@ export class Repository implements Disposable { const diffEditorTabsToClose: Tab[] = []; for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) { - const { kind } = tab; - if (kind instanceof TabKindTextDiff || kind instanceof TabKindNotebookDiff) { - if (kind.modified.scheme === 'git' && indexResources.some(r => pathEquals(r, kind.modified.fsPath))) { + const { input } = tab; + if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) { + if (input.modified.scheme === 'git' && indexResources.some(r => pathEquals(r, input.modified.fsPath))) { // Index diffEditorTabsToClose.push(tab); } - if (kind.modified.scheme === 'file' && kind.original.scheme === 'git' && workingTreeResources.some(r => pathEquals(r, kind.modified.fsPath))) { + if (input.modified.scheme === 'file' && input.original.scheme === 'git' && workingTreeResources.some(r => pathEquals(r, input.modified.fsPath))) { // Working Tree diffEditorTabsToClose.push(tab); } diff --git a/extensions/git/src/staging.ts b/extensions/git/src/staging.ts index c2af4a575b5..36e9c6f26e1 100644 --- a/extensions/git/src/staging.ts +++ b/extensions/git/src/staging.ts @@ -109,12 +109,28 @@ export function intersectDiffWithRange(textDocument: TextDocument, diff: LineCha if (diff.modifiedEndLineNumber === 0) { return diff; } else { - return { - originalStartLineNumber: diff.originalStartLineNumber, - originalEndLineNumber: diff.originalEndLineNumber, - modifiedStartLineNumber: intersection.start.line + 1, - modifiedEndLineNumber: intersection.end.line + 1 - }; + const modifiedStartLineNumber = intersection.start.line + 1; + const modifiedEndLineNumber = intersection.end.line + 1; + + // heuristic: same number of lines on both sides, let's assume line by line + if (diff.originalEndLineNumber - diff.originalStartLineNumber === diff.modifiedEndLineNumber - diff.modifiedStartLineNumber) { + const delta = modifiedStartLineNumber - diff.modifiedStartLineNumber; + const length = modifiedEndLineNumber - modifiedStartLineNumber; + + return { + originalStartLineNumber: diff.originalStartLineNumber + delta, + originalEndLineNumber: diff.originalStartLineNumber + delta + length, + modifiedStartLineNumber, + modifiedEndLineNumber + }; + } else { + return { + originalStartLineNumber: diff.originalStartLineNumber, + originalEndLineNumber: diff.originalEndLineNumber, + modifiedStartLineNumber, + modifiedEndLineNumber + }; + } } } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index f2e665eaafc..8d65812a8d0 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -169,6 +169,8 @@ export class GitTimelineProvider implements TimelineProvider { const dateType = config.get<'committed' | 'authored'>('date'); const showAuthor = config.get('showAuthor'); + const showUncommitted = config.get('showUncommitted'); + const openComparison = localize('git.timeline.openComparison', "Open Comparison"); const items = commits.map((c, i) => { @@ -220,6 +222,30 @@ export class GitTimelineProvider implements TimelineProvider { items.splice(0, 0, item); } + + if (showUncommitted) { + const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); + if (working) { + const date = new Date(); + + const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); + // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? + item.iconPath = new ThemeIcon('git-commit'); + item.description = ''; + item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type)); + + const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); + if (cmd) { + item.command = { + title: openComparison, + command: cmd.command, + arguments: cmd.arguments, + }; + } + + items.splice(0, 0, item); + } + } } return { @@ -235,7 +261,7 @@ export class GitTimelineProvider implements TimelineProvider { } private onConfigurationChanged(e: ConfigurationChangeEvent) { - if (e.affectsConfiguration('git.timeline.date') || e.affectsConfiguration('git.timeline.showAuthor')) { + if (e.affectsConfiguration('git.timeline.date') || e.affectsConfiguration('git.timeline.showAuthor') || e.affectsConfiguration('git.timeline.showUncommitted')) { this.fireChanged(); } } diff --git a/extensions/github-authentication/extension-browser.webpack.config.js b/extensions/github-authentication/extension-browser.webpack.config.js index 0722e3e572c..4fa2d1aa902 100644 --- a/extensions/github-authentication/extension-browser.webpack.config.js +++ b/extensions/github-authentication/extension-browser.webpack.config.js @@ -22,7 +22,8 @@ module.exports = withBrowserDefaults({ resolve: { alias: { 'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js'), - 'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js') + 'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js'), + './authServer': path.resolve(__dirname, 'src/env/browser/authServer'), } } }); diff --git a/extensions/github-authentication/media/auth.css b/extensions/github-authentication/media/auth.css new file mode 100644 index 00000000000..45c42c75ad5 --- /dev/null +++ b/extensions/github-authentication/media/auth.css @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +html { + height: 100%; +} + +body { + box-sizing: border-box; + min-height: 100%; + margin: 0; + padding: 15px 30px; + display: flex; + flex-direction: column; + color: white; + font-family: "Segoe UI","Helvetica Neue","Helvetica",Arial,sans-serif; + background-color: #2C2C32; +} + +.branding { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACWCAYAAAA8AXHiAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAlqADAAQAAAABAAAAlgAAAADkcSUjAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAAxaElEQVR4Ae19CbgdRbXu6j2cecoECTIkICCGzAg+7qeQ9544QFQgiXpVEJTEe59ALsbMwE5AMZCQELgKeSoqDlyiQogCSUAC6FNCQhIwQMALCbNMGc68p37/v7prnz47++yzzzl76OCu851d3dXV1VXVf69atWqtVZaUg397IPJwSCKT46zgx16wh+19XSYnO6PjE7H4MDuZ7BSxXhQJ/On5KTWbTSMm3bYlvHXmJNxj2SatFLFVioeWn5lDDwAgMvOU2NG/3DMo2h691rLtr4bqBtVX1NVJRUOVBIIiyYSI3RkXO9q5NZmU5S98tu7XpmQF2AwAzCoNwMrAMm/CT3HkrgqJTI/KwodOD4ZDa5MVdUPtfe+KxONKiSob6+SIE46XqsYGK5FIBoLVtQEJiSRaWp+xktaNuzpqfirTLcBOZNJtdnjrDCk6wMrA8hOgWJcZoFSrT4nJogfPESu4Dv8AVLTTEqsC9MkSDHB2NIYfkZGnjJOqpiZQrkTCAmWyKqtDgXBAEs0tLyZtWRl7953/u/uiUR0sVgH2xuKERCJJnhc6lIFV6B7OuXyAJrIpqDzVlQ99TUKVt0scAEomQaXskBAONlEFrgpvLdkZk6r6Wjn6IxMx2qVeY0IsSVrhqnCgMiiJA62v4a5Vkmz/4a7PDWtmVXSIfGMSAGYVFGCpGvGh5VCiHohEAiJXi77sKx+eLZU1N0hHCyuTAJCCDqCAKIUCYxv4AcY6YnL0qeOkZuhQSeoo6b5OG+AC4ZJQRThQFQbAWt7Bvf8ZSNg3P3deA8ZUF2AzADCrMAArA4u9XMoQsQMp6nHlH5dKdcMcad+fJGVCCKRAxfMU1XKB1dYpI8acKI0jj5FkDNSti3LpzSgjCQQmrFA4HKiuIMAO2EnrtkBb+8pdXxr2OjOd+bAd2vQ2cro8mXPjwH/LwBp4H/a/hLvuCsr06cpky6KHfiK1jRdJ235M80ClABMtmMMfKJQDNB4j1VCstg4ZcTKANWpkZmCxAJZiABYIhgO1VRwi21HGj63OxI27pje9xGz5BlgZWOzVUgSPjEqufOheqWmcIq37YwBCWKsDDGlQULnAUh6L4CLFAh/fCmCNOSE7sNxi3IilxiUQCAdrayR+oCWBcn4q0eSy56c2PMc80+6yg2t4MEAKVgYWO7HYwcz8Ig9XSTz5kNQ0nC7tB6KoBmZ+bnAZdaVUhIMHVDxWQtQCYI3thWKZ8rrHLICiCwCslmIKFGn/GoPm9S+cW7ddswJgZw4Ta9NkC/n6HsrA6nufDewOV/Ap8x8cggHvMUztTpLO5igkCV2g4hMIJI3dY56mqBdwgXOlWP0DlhbNkhVgtoQDELza7Z1iJ2J3J2P2DX8/r+Evmsm2A2dukkBfAYbZSDkUrQco+IQ0XeY9eKwEraekovYkzP4OBhUrlGLEXYB1q6QnzXPYLUtuJ5SLcei1k62tMYDKDtTWnRusrf5/J9zbcv9xa5snc9aooLJti3xYbsVy1lEOxekBI01f8NBECVnbJVx1hHS2YSrnGf4y1SQTwAyYTJzpvr6lAWA2AWYlW1vidrQjGait/VS4pvaPx9/bvOn43+4/GwCzDcAoC0P+rKNd1ot9q1s5d489YIa/BQ9+Ap/yAxKsgJSJ8gGXUc94I1BD4Og/j91zDodYGFTmvf88VsYnpiWSt7ICNXWcoQoo2uNJ217298/X/8bky7ZcVKZYppcKE0OaDg0FDn9XPfwFCYc3YEYGUEUpTQeFUNTgyZniDBUyfJdecu9hVJjAYS+YbGuJ8z9QXXNaqL5uzQlrm3d88O79X+Ujt860MIu17ExDZJliFealkMJYMn1NQNZATnXlQ9+ScPXNEm3H02zKrZQK9PpoxQ5+PBJ3pVwqbkBJpFhjPiSNx47sWY7V60Nyy4BaQDQhdqCyJgRWX5LNLbuA8xUfaKz7sTtEkkhhMuBoU5QpVm792rdclKbzkyWoFj64WCrrAKo2SNMxhukSjb4CvoZe/pGBebyfP88ZTNx14KQX6BdV4McQSna2JZItLXGrsurEUEPdra8daPnv43934CIy+Qoqth2hDKx8v4hpkKbrAi++3AUbf4AlmqukvTmuiytgWfr3OBdg/bs533cpwMDgJ6BFEQtUVB4dGlz/kxPuab5TH8S2A1z9bGi+6/o+KY/8FKkUw6KH/ktqm/4Ngk8y6UHltvVCH39IrQx1SsXmoI9l5Tc72xS2Y50AWGtnaEjdF46/p3mtPuJqqmOUQ356YMZtYai8xITrfzsGb4Dg8392W6IZ8FOygImXslwe8KOzF0Dti2D8vZbO0KC6z37wdwfm/d2yvu8dvbPfXr7acw8YcULkrw0Sa34EoBovHZCmq3Jez7dlvwKkGMCoqIHn+E8tQtse5n1UwZn37HXVq0nOeO14sj0Ws44tD4U59FjWLEaaPue+IyXWskMqa11QUfCZQgaK6OtxhqeaIkpInjLUyiRBjJKMhRrrqkOh5IXlodB0S3/ilDR9/cngyx+RcOVg6WjtXZqe67OUUqVnJroQsgu+nTzF/oXUlsIUTIk/WaZY/e18A6r5D34MXbkV0vTBEoVKZ1Zpen8flv2+FCHLnq0YV4N2Zwcwb48pU6z+dDfVXiKnRKGh8HksJt+tIp44penZlmj68yBzD6DjRQ8pmQbwWR4u2aSau0oQg2JRT9EaUqZYfep9vEaKFGhFs2DDN6Si8m594Yk4RQyF+0gVMRlgwyTzb9qRIZu51C02+Uzc7WLaSS55eIvJh1WHwnVGWt0O+VM1eFgMg4dIXBY8OF+qar8HlRd0JciHRRutEoXUy/Q836R5kjIemnwmzpjJTcwlD7O6+crAytaZ5ppK013B5/wNy6W6/goIPkmlMBD1V5puCu8h5gs66GV6E7zHPZRRsmSrgOS7ZI3K84O9Bg/zN/wcuulfVYMHZ+3Mw+Hk+bmmuHT8pPgrZki/aG4qfVymWNneAfmp6Y5TDpm34T6s+306v9L0bA/ntSzAMZdK45qht4qXKVaPPeTM/GISWVcjnZV/xPB3mho8WL1ofPZY4AAuGHmWAZMpKv3cpPsgLlOsTC/BLNHMXn+YRAOPglE/ERoK3a1oMt1XrDQdDsne4YEEl/kv1vOzPcetUxlY6Z3kLNFEZdGG42Gk/hh00w9Xg4fedNPTy8n7OdBjgKRl+5RcudUqy7G8ADDS9DkPnApQbYNjjsOlsz1/SzTeZ/V2bKiQiZk/HUvp572VWcTrfaNYVLddvTUkgyYl5Rk2c1NARtRbUkIHX3nrKyNNn/fgp6BCcL8E0DUxuHShNL3ULzBFqVCR1KwwlZi3LshnQbkDizMkS61i2dkmqDa2zMQpr7tuDc3FQyOmNH0TtD4hTZ97/1ckFLxDEniBCbpvUZMofzQjHdzp5/6oZaoWuQHLMLOrXmgQO34hvugzUMJgfMnvIH5I9rXfgRfTJpqv9P4vU63r7cAYPESmx2X+A7NgQLoCtn68q7BLNL3VK3U9DT1pp6lsPjzoBVj8mqFWSzOfW549B7ZwP4W67RB1fgm7ACh24T84DZTsKlm2/WKZOX499Z1l512OdYoPG5yqkho8qBAoIXMf+K5UNiyAch4axbdXwiWaVAU9BwZQqdHPJCAPD82/55aSHbp17BlY/JrXQLVmOoa/VTtnSqjqVuFI2LI3itkJmX6nCLrICVceIRVVD8iNTy2RK6yrtVGGypWshVke7Bg8kCqJzF2/Gk45LoGMynEf5HGPl6WEIl/yAIlPTjstcmWyP86tW+ZZoWvCo65sbvrblVLVcCuGCFtiUb6MCjQshH/oOquAtQJMbgKzp6TUD7lKVjy9Ua57bJAaaUZ2dnd0kb1KxblKXtAYPMxd/zss0VwibTB4sEGlaF7sl8AXZP61TjhxX1qqirYO2alTPx0c3JHetbGbdq6SmqZLpXUfAEUnAo7NWA8NYLNjkFBXQJj4NpxkTpMrJj6ixgXPTLNTXut6uLkoyTR4WD0zpg5kB7+3EbrpZ6g0veQyqh5abyTuRBSP1dsM8iYwnMBTn91ph4afdKw0jDrWtqForq+oh6KKmWyDoegOLO/MbuXOO6V20BekdS9ngRwyu+ftsaZ2DPIfmI8je7Rtvlwx9vuatdRDo/FJNe/3gzDZg8FD3RjH4KE/SzTsinTy0WOH9OFCWrl8hD4GPwZYSSj/wmzBqm2y7Ff23H7cZ856Lzxk6LfjLc30nFw69R1PKwmsrqGQL57iAg6DK3c+CCadoOIyhnoh8dzXy6EVhvvohMQ7bQyN18mNf1sHUURdSYdGCj6pnDd349H4RmDwUD0AULH5hQBVT+WaZwF0YEYkGA5KKGRZrfsWyS++fHGgtvofYEpArArjpLaXl93jZYd5N9SE4oTkM5vAd0yQln1k0vvLI/HLsZXa1TbCX7n1nNyw43z5zujHBZ7iVLhaYHfQqRYbafrcB8ZhDHkEk5BG6YD7oP63LVV04Q8AKhIxGuZjsAOVBZtx4C1JJKYmb/78Y/r8aKLKrsYR1A0NBAtfryxPcIkuPPbajjeUVTuOhBcUfM11E6Aa4iy4sqb9/4fPJVCvNrhADFd+QCqr/irLd8zSCYFjht3zjDRLvft0SakwdniYd/+ZuG+LhMKNKk03fj77VFiJMiuDjnGwuoG860a4ED1RAKojbttSwxrB7zYGHuc1MS55AF4IGUjLIUO46RksuNp/lYpqmC/Bw1x+mVln1mhB07Ju6AoMjf8iL738FTy3UzhrjIzm8/IfyKjTfdCc+6bC1eYa/Z7jMcfggS33BsPamDjTtfQ0c55eFtO95XiPzT0m5jUGU0Z6XhvyQ3g6ViXVtr2LZeWUiOZH2044YVL0dT3x5w8oFhys2rIeqiEwX2rPN6hMqx0n+G37YuDdpsqoY56V67eNV1DphCHSxeuZO/obU/7GMjn7m/PANyFNX6MC3XiCzvgpJjk4mDQTe3P0lMb0TNd4rzfde+wt1+TzXk8d66gWxUYC8P4f2yvtLWfJjQAV9e7JA7NtPg8haTzsPzD7GyUt72GbMqksYH35PWJoxDBbUTUK85ptsvypb8q3x96mz/TOSPtbCTV4wGvlJGTu+kWwSr7GFwYPfWoP51RoA4e+1n2PSqdMlZs/97aKSCKngOJGUvDrXiyTDQnsfqX4Z7bOCqc6DsHwNRcnYGjsgJQbHVE3+FZZ/vQdcC7u7CGDnar6XQUtI+L4aJpz/01gdK8BTwL5Gx9Egwd2vJ//teUY+kIBsCRBgGqpLPvMGXLzZxxQcVbrNCCti5wFBCfRL+2juMGW0QoseAzRd8D30O2fzUlP6+nc5DWxN583jUMSCm2Dw/y6QV+R007aKUu3nqRrkpxMcDjrS6BQ10jT59z/K6luvAyg4syPQ6wLqr4UWIq8Nih5NYe+Vuk48FmAap72Az8YB1Q9VwrdfNA3w9wm3RybOD2d5yakX2N6+nWTZtLNPeYc17lXC10yp93NcxM8uU1Sj7HJa2JvRm+aHjueelsh1qiqOREzx7/Jsh0X6mSC7gY5NOYSyKRz2xC24zv3rccQ8iXsRYOv2/ECnEsRpc2jQ19ch77Ols0ST5woy6as06GPn5f5YLJV0tu1Jp83zRybuKc83uvm2Bt7jzOVwTTmwT9dGu6ERS+TvDSV58UKFViH5EaNAfB6PwXftVofTD6J4oJsQaXpYGRnr6+V79y/GUs0ZykPR9/lbgN9G7NdNjeoDGLoqw2h3jfJDWefJiumvJbaszBX0VTf6Hu2Hs3bNQwT1m+kgiIRiB1KFSxYZFNLghoGdYMvkRuf3iHXbzlOxQUEV6ahcZorTZ/1wAjoiO3AyzkFwx+c8XORHKjiv5+DLRz6sA8h9nZuOzBdbvjMLK1uLkOfn9vl1g3A2r8CM8I9mEFV4qU4MiXvOzHH3th7zIK85+nH5rynfCYdW2ugnJBK/Ctrx0qw6hm5fvsXFFwcGimxN4HS9DUQfH573YfwPTwFg4fjpJM7PKjmhcnlV3Bx1ucOfe07xI6dJDees0Y4pNPFRy5DX1cLfXsUkCtOxxZjyU/Br9M+fEFUiekClwFFeszmpKeZc+8102xeM9dNbPJ5z538FbrkEghWQOZ1pyx7apUmczcqUi/VTQeoZq37H3g9T8J90FDpgMGDoVR8kLdMf1EuZ8ivqgthdWO1LPv0eFn22ZecoY+yKX+syjivYWC/kLxjFjYba3k37RwHbYQ/gU85qgDS977WMgyhZhL1SGJovFSWPf1ReIs7T2aOe1ULunTt2Vgp+L0EQcSiEF1YWDqiSoluDwKGg++HE0vDexhwmfO+1iYv+S0ubVHUksTQ9zVZfvYdWqzK71SU0L+nOFIv517vB9W/0gZ+l9vHkOSCtyIluHz0yxK1x0GV5GmdoeCVDfwpAyqBogJ82Zg1Vtd/RIKBF8B7nSUX/OJf4drz90qVYjHOBimecKgUY90ShOfuMaJU8B6nEgt+gFePpZlqLCBH25/DBzNaQUVAkXfMhwEK21Wath3ceW5d+PIkxSTPH7tXmk+eiOn6I1Dwo2ZDqcHF2nHxNY79jaskWLleRh33S2irQqVQfVIFHSDh3XHWrpQJLdPGeWMee/9ZbFECJkSY7VbVYcWh+eey7AmAaspz+iETUO4uDgOuSUkpcebad8mKuGCrZFlnh2fKsr/9FtP/86TNVfSDWkbmIoqQylljPEaib0lTExaGwra8/lpQosB9GE1QIOEqPxPW0tSUMa9pwEnqnIkmk3s5/1EUCo8V0E0TiBJmyvJzVusj2Mfs6/d5cCiWaSS/IkqxGWaffD70qWBoMIgyIcq4Uq9Irxfzx3myA5t43JKamoCMHClSXw+aSlkoMpihzwyFhKFJ1/vdPE5ZXdfy3Q5oTqFI6E5BRTvW8SK2lx+voNKPNgLWA338TxC6A4sNphRbjSkw/s8+eaa07fsurFhI2dhhfF2lDWTQoaggQVTpA0eJHHY4XiPOwetrDVNgwrnqiBtApRDl5DOtYP78BQ5vAlCFsSxzl7zcClHClB2iyoYAVARrmf8koWso9DZYFfHwdX0Y9oHTRy/CrOxt8AkrIZJALjpcpm51Xl+I9+m9H3MU05UQxEOHiVRXibwO7SRSrwoOjahbgJkQWE0e8oVztmji9Fkj0wcWMPRVVKiKTtuBy+XGsx0xiVKqyYXlVdlG/vshuN2YGVisoH5dnLWoOOImyJPeAc/wC3QcGGa4xnUMLErfFFaltk5k5CiRN94QaW4GuCBrVPGDCyRvLfkC2Hj+6zEO9Nh9MzzOOWhm3hhXteGOlldBTc/HssxmxzrpGbsYQ182omua21OT3FZrFzBPT+e85u0ak8+km3OVxOGkZ2DxDn7iEXQaxREzx/4SDD1M6m3sEAqO2ewQakrU/O6Pvije7p5785hrvGSu89jk8V7PlMdcN/eS0pih8UgMje++LfI2qoklOLXUZkt1wOcDkFfv1x8cI874Vkw+VgCB2RlMHXmsRejegwGIZ8JYjlon1rtfkhUXtOrQNx1C3CIFVstbtfTHZrtm8qbnST9nvkxp6enMw/+DeSzmTA86Y4Qa8eyT16NHT8UMrRVGCWTqM89uTA3MU7zlmWtMM9fT09Lzm3zmHhOn0vGWafJPkAw9TOQoAIygUb8eyJxi6HE9xXchnflT/ywUQctkunPaleY552HStZgJhi3M+uZBNgVVF4CKSzOR4oGKVTG457FfQm7AYm2pm04d9dknPwFqMB4znjexvkjtg6J9mVk7jUBiIPWqw2zxmFGC2SNqh+oRPBnB5QJIQcRjPXDKMYBzzry/zARVH7Q9EXsL1uEfx1rfUkdZ8dBQG/Y2plDHuQOLNTDgumL03/Gmxkln6y61fPYLuFhHMzSGgfkjj3aYezL1pGgMBjA8V+rlUjoDKr3uZE3l7zoFakHLqDbc0bIRVk0nyopzHtO1Pi4eF8ukras+vj3qG7DYDIKLPNd3xr0lweYJ4C0ehx2iX6T0TkcTXAZIFEccBYBxFqgyL2QheMyQmKJkSNc0FpGJenHoCwWx3hfAWl8EVOosWXnuPh36etPwZJH/ZKEX5r2H3lCeC7PFKyzunv1Ruf6pP0BK/xnIvEAaqFvFt1jqgCpw0OLQWN8gMhLKjG+8JtIK/1ecNbKGJGLpNeU9BCbTnWOagsZgMQOFRGiAWPHpECVsVIuZnR+Gh8PpmflM3F6UwDawnvz3S0Bd+k6xTOW5eG10pOaMPRuLxXCuTyl90hkuTL6Sx0AIlxXDIKpHjRQZMsShXPTa56VcqSHQTdcXBbJnQ2BWSWPR1kelDUPfcoBKVXci9vtFd6oQr6j/wGJtqCNFjUeGOWMuxBLQcixekwriO/KRLwHv0Hj4CPBeRzqgov0qAaT8Fqps+C4m2kkYY0BmEa4KghIvlZXnnCG3nveWZ+hT6LHp5XBwD/RvKPSWo0wrZkMMc6zZGBbfgT+t66DRyY6nkagDPM1Qwh+Ci4FDYwMWsisprcfQ2IGhkYw+KRazONloLIq1vvZWiFa+BJP2dchgybQ1MBYt8dCnjfD/z8AolmkfZ0NXA0JcvpgDt0Wdzd/Al45XFODSD4dG/wQCjOCiAcnRI0WaBrtMPb8D12KmAgvIHa2bpS3+IQUVhz6G94nasLalwD8Dp1imgo5ukSulH/NjuQGUywrdA78JQch7yGLmB8TmeQONCS6CbPgRWDSGu5Z/vAF+CkNfTS1mfXtXya1TL9dHqCXQIaLmwm/DBFJe7znT09PMuYl7upfXGdLLc1K7/7p58/+yOWNUccTYtVhS+QgsaPaBcPE5BJd/gndoHDTYlpHHBeB36oC89drZCirObA8VixkltmldmwkE6Wnm3MSmiEzn6Wkmb3rMfPjPP7D4oK3u0/78Z5FX9jhMsVq551o79/5iRDo0gomvAs818liR8ad1PXVa12H5qA89AKqVf2BxrYwCw5lrPieHD31C2jub5OUXk2CCAw7L5UNwcUxMQKHLCjTAKvsPct2Om5Bkq24aqW859LkH8ggsDB1k3uli59/v/jqMYO9Rvj2AmWFHR0BefglMMhza0LKGMzD/hQDUgRyj2fpBl8l12zdL5K9HpuwBVHTvv0r7tUb5AZb6bVoMYEFL8lv3zINu0o/AsBM9CTCMWAbBHCEOATXB1Y7pvU4WcZk5/PXfZTRLy6Cq6ufke09+VsHFehq1bb++TR/Va+CzQjK4EXe/5MvWLoerSeyXDPdBAVCwpOvFlxQqRHBhJrZnt7M4TOU8MzPzUYe4VaG4gS6FatGetfK97ddjaJyLa47RrB+NIQh8PwTOClGXgVEsgsrIdi5f+zOs+l8Bwahj7UuzJz5E//mDgDVcPX9lt8iB/Y7euj+HRdbWMZqNtiakfvAc8F2PSmTHYV1Do7aE+UobONf2C6jYE6gL33b/gUV+KgWqdX+AT6oLoOnARWhnhwcDKM669JhPRSCPxf/XXhHZS01PHPuqZ7SW5of9E3CNZj8mVbJLrtl+loIr4oojTM4Sxn7CFbuB9enfUKhakpNj8h93VUuyGvsl130UVilw323R94MTCCg9YYygp7hI1RQaOpB6vQEDCA6H1PrkOp03v97kgx/HnpL6V1CbqWgCv7hevrttsSy0Ilq7EgtQ2Wump33QW6kq9B1YpiMvvW8YhJ+PqdO0Ds9+yYoj/Ojam56kHqYHmgRwMaZFzVtvOuAaNtzBlR/B5bSAGqPYGAFbi9QPuVq+t+NfJJqcJpEJ+4QuLrlDWjmkeqBvQ6HZ4eGydcdLAO6DKqpPhI4SVZOp6OeARYHhAsekKZjwo8OiJ+Z16kbRAOLNV3nm5NElO5RhyvJLTHea3MiJ/iRq6v+3VFjPy7VPflxBxRmj2mNqK/7pf3IHltnh4fJ7P4IZ3zZoUg6Hdxp+pQ6oTFfqEOieKKBwzDh17AKLwyH/mR/meLJ/L7QN9jhDIvdB9CtTr0qMNneIiKEPhmH7lEfku9vnOoa+WIw3C9amP4oR+/AbzA1Y7Cxanlz++08CCI9DRbcWPgnIqGeWSnvBpR0L8JiYh/qPH+bTf6RREa8FNoFcAqKtoJ/B5bQFewZhO7141IaD3u9DJLFOZu+o1VUHP26np3Uu3k8vwDLSdCzRzFr7ZciiHgAQsPwBuyobfEW2LyVFokxjCCIee8BkQMV02v8RXNSPenk3zOYxwhpBKm/LFkw9TB6eDySY+03MsjIeu3I6qmTXYM+gwfYuWbLlNLULoHZtUYdGVtD8pzc+vfLefOnXvPem5zN5M6V3v9YzsChNp2Ibpemz7p0l4dpfqOeUJFSPnQ0wvTXo4ViR5FxLgYqnBlxpMTuGSnc02SK4OqBST3AZIwfTnvTYeUL3fk3P05dzlsf8DOa+no45a6QzXe4ZFKr6APjOv8o12y4v6p5BWlHvj6m0idMrb/Jma6S5Zu5Nvyc9vft5ZmDpEk3E0em+7N5rIX1eIdFWzGyVq6bgKfdAqmSCF1wpapUJXHgEh8NXXoILoBa/C1JN6+CyqBOUPI7t9AatxKzxLrn0vkp1bPdPODQeDCxdook4QqVZa2/D7GchZDjcL5mwQH7zFfQh5p0KKjc2YPOCy0vF+Koo5+KzXtkNfwz7HHBpAp/r2wALJdSNGyPUNE6DEuGzsuTJcY49JgTKzpYsvq18PivWHVjdpOlrfwvd9RmuM/787Jes4HIB6QUaW6Tgw4+Czb1IqTyZ+NdeFtn3btKR0pMZ85GhxsFvg5V39wyqHgWB6na5ZutMZSnoaIV9XKrQn2+yP/egfV2NNNJ0xjXDN0ql2S8Z0vR8BgLHK0pgxV0cOY/xnDAflU8tGIu+9aYzWRgCt0UJOF9znMH1bVjOZzt6L8vZM4h9zD2Drt3+LxIf97WUz9d8LWSbBYtcAZBrPm/7+noP8jsUS5XzoEc16+4mqR6+RSrqzkjtl8xCc/lnRUw+7zHTTEgde8Bz0LCIzLys/5oPFjPwkxCQDnn+2bPgYO1fwSTjOrh6G4Ya5pn+jDE0grpS5lXb9FUJP7VTIls+5Kw1gnL5wrDXvJz8xpj1oYFUzrt83dGSCMAZf81Y8FQHO+Pv7bkp0CCj95j3mZdujhl7yZSCCyDSYZCXcBzACwnAKLYGxqLxts2QPxwvd39zo8w/+ddQGDxHqR73Rwa3rMX59kc3RjBD44cw690JvusCHRqppVrKobGAfeaIEy5feyImfNg2pOooSNO7lmgK+GAt2lArnihxYkyAYeijAUYl9pjpaF4lt00/TX70lVfVWJSqwgvH/AE5T8eQ2AEzMw7n7jpdOqJxJWPIlC/XtPQCM92XMQ/ccbt7BtU1/QwiidWai+Kc96H6c0C+xh1W7fWQvzTBNVHxQGX6vhu4lGphjxnsLCrJKDZvmp4yw1KLGVBW9RsBd0oLxv4FIJyAOr+NZRXkx7ZsGviie/tnxvQ8uablcl9PecBv2VDj0D2DBl0i127bLpEnjtU26VIQBNLvkxCQ+v1XQJfqGHxNUEjHGthBHZ7eSQU4d7qTbChcLtLbMPaYSdgnyQ/Py7zHjHGntGDscxhaMHS3/re6asy0XYvBi/eFsQnpwdsscy0930DymHsdvha7fWEhu7J2HDZAfxYAm+bsSYih0fjDMHXoT2yeZervPc+WxmeZ6+bYe6+pS7Y0Nw+1PKfiq+dp1wzRFFC8OA5+Co72MfS1H1gtt5w7Xn54/otZ95gx7pTmjH5T4vvGw2nHFvVbZcDFupsO8B6bjjPXTOxtqzfNHDP2BpPONHOcLY+5tysvvdeg3dgzqKr+Llmy/SbNYvYMMvlzibvK7A4M3ptep2xpJq8pL/3ZmdLT09xzzgpH61INfSyYTMWMsYCDWV4Iin5JqDVfAFDN1PbopAJrlNmCDos2BI+TW2Th2FMByg3ujhrZ78tWZlGvweWTs2cQttNrugx81+MQS3xAh0blu3IbGv04ftIqJTOqC93BxtF+pTrafw6mYqNl1bl36CyJ03AytbkE406JM6xF4z8Jge6vAK6wTgDcvWNzKaaEefhxO0NjVd2pON4li7dOUXDx3ZC37CUwmwnmdZrYpDP2ppljE5t86efp96XnY34TzDFjSh//Bv/kvAYd4aIFGFzgj3vMdGCPmSHbR8stU59z/E4BUI4fiNwr43WntHDcl8G/rJJqgIu+rfwtpfe2EbNG1zKouv5eiWxbqv1Au4JeZo0GDObFegvNdC1bPnNvT/eZdJOPsTfNlM09oddgys7ruVEI5hxIsDj0VYKfAwFvb54Jby4XYg3NWeoYiMtFvgCqqZDaLRp/OcB1JZakKOci3YLDD1Ta7/9JWgbBRq6TlkGD5sjiJx+V2X/2n2VQDu8/IC17V2L42A1xA/z64KUXKujQR5eL2F4tFn0RBHI8+KnVOvSpNkWOQ1+2+tGdEgP5s0Xjr8WOW/8GgS9oozqOKCZFzlbL7NccXjfoWAY1fEzqq3fJVds+4YhZImARXF9k3lL89MGwXqhPQH56UQcsts6CuOE93ahR5UF5rimXXshhco+Z9pY1MrTzJLn5vB3gHypSi7PejhrIsUqzAVLKha4afytEF1NVpyug6hLFocoDqT/vdWR7mDU2O5ZBVdUb5KqtEYey4+Pxqj87n9JAn5i/+wkdBGdJZ9WUFyCQxA6rHS/hCyfDxSWd/AwdOuurgHYEFNw7sMfMLZ+frmrOpCrc17lQgcMqeZNF434LmdhkDDHc8zAEgB8iM0b2P3eOhWVQtD0J58FXy5VPbpBZ25oo83r++TfyqxyQ5/fgLOlwEfrm818Vq3U8KNc2LEI74BrYwwBNDH0sK9b5GmZ9p4FKrdJZTr6Gvt7qZ6T0V47ZBHP5SRCr7JMwpfQ+B5f71euXzaGRE522/di0oP4TUpvcJXM3f/z1mUdAhxvB5jDvv+BUiovQpCA3f+WADN1xCuRJD6oEnIx2/4KjdVCFPWY6WtZJdcuJcvO5m3XoUyZbFQn7V3Jf7zJS+oVjnsIEcTx2Z31FKqAtQUGqUmXi32f/rJjWCY1lHeFgCb/caTYGqnsY+OFHAnO3LGJXBMIWWBkeaU498MOPUyVTE4LLyI++dc+vAa4vYvji0EGpfPe85p70mGAM0CoCIR6dJ/953lI99patCUX+4bBICjbvsUFwB/kIpPRj8KL44fhzSCGgGBh3Axn41aQdsGqHWPZbr99+zLgT3gsNGvrtROuBBF5QrzIvLbPQP6jzwWDxOvr41t2rAK5LIWvijIp5s5FdKi9j6KvlUsVbYECnQpTwmA59o6fZUHArPZtJptfwXv8IboT68Bk6xPgRXClg4cALLvZiEjIVKDraiXBo2PB6qR9xmG1z59kMr7PQGMpYPup7MFCcocqRB91y7mUA1VVYLOVyDypuPCBrS1EmYwam47OiFL2zZSPoG4Y+BVWFOg7xA6hYTYKKlJOU66oJZ0rr/rtTS0CHgpReuxs/jmVQCPywI0JxztlC34SDKZapGgWN02H+RaB963ffxHreDzGzwteC3bNtBSTvZZOgkIehjzxkLBqRH5y7WIswWqmmPD/FNIfnFsUMS7bDYKRxBgSqFEVwKOm5T5i/WKELROxl/W7R06RW+g+KJXZnTIYdNVzqh9ONBprjVUEqVj0zPQd1660TIZB7GIaXkAv9+2+mADy3Y2gcgoVTp6E0dCCgOg5g1pe8WH4wbQNkLQHhHjMEpJ8DBY0RvjK8osg27HvduACCYjNcH0zJi90W1Ayd7DyVkbGtzAiswwAsfBeHELCchhnqM+OuRqkIXoD2noEL8LxvvY3GPCTh2B2yYnq7sx3ITH75bo84t/v210uVl2ybBf5wBfhDNAvrpqXeUUN70O1Gwt0w8OnAOhIUa8ShCiwio7dZXW/X/YsuUOVNDlVevA1uBCp+4Qz5HPd1NlyamucCrA53KDykgcXu5Rc+c3VI9g5KymhspE3m//UR2FptBumw+3mV5j0M+KlGHBHZ+ikM7/dj32sMP7EYKBcFqsUP2YCFncu681igWLQcTx8KWUZPzE62awNtLcru6bEDLfrQvJ+m8CpQ3X4q3tzD0PqogXYt5XjFB1cmYPXIvPtvKCw9k+onCBopfWT8ZkjksLzV+aYadqSk9KgsX3gx/k2/pD/LpPs8LgMr/QUZXfprJ2Bh3sK+123P6/JWIVWK0utgzg2ozPkhNL6UgWVemjemAJU8VwT7XjcMHQ8h8eNqqFEIlaKeyB9ngb0FD/ByyN1baXm9fgh9A3ltd26FRWysnVqcHWJW/OR9kNJ/GoLU4vBcBilGzGBil8/iAhkFpEMpIPXhrLBMsbJBzBhqKLAmcjN17HsNXXpHjdu8+mwl9P9apk9eKRR+0p9szvU6HlnK2G1xGVi9vXqvoUZk4oUA140w8KW2B2hGARfWDThYP++x1tckmNjNo9dK/ONWKdN3UeKa+fTxRtecC+pXbZkPcH0PqsPU5GdX5v8DZakMZgg0cZrkXYfC4Yf7bkkn/x3idMf775eAigBEXGFYcgo3U79EQtXQ7cSCKXX6CxIMunoo3KUOPVwtaXIZWH3qfi5Yu4YakQk/go3A51XafUi4U+pTQweWGSpIZWD1pwtVrwtS+iUT1ko8/nFVJaKtZKENNQyFMnF/6l7Ye2wrCIc62GukDKz+drSR0l8z6TG4s5yIPXbew9BIXXqKI7oYbgOCvsbeepl7vWneY/JfDMqH4bhEMRj2hAXjZxCsp8vAcl5J/34NuK4av1OsTpjPte+GQa5jqNG/ErvuUjDhVbmY6bqQ6cgFU6ZLRUyzEdRnbFIeKANroB1vloAiH31VAlEsAbVsd6T0/bZw8tQoJ1Qhvy8m99ysPZxs2d8mVujnZWB5XmO/D7kEREONyEcPyMkTTsEuFX+ERir2boRdZSGDUrVCPqAPZYMFCNbX84Yluy+qe7MMrD70XdasxlCDAtVrJv4vWP+sgadkDIsElzI9uN0gIdfYPNGlXOnFmMsljdFeLNAHmxorE/v33bv7oqalaK4/rWhL2k8DeThFEcaf1ZKJ0+HY4wegXOS5YAuI31zx5OJI8cib9L5UImpojgdS2QHfS0DFrFA4GKxrrEjs3/9fuy8e9DktdbFaQgz4AeUCvD2g5nOuO6VrJv0fDItL4E4JogjVsOUScm7BYMfEB93FC+b/oIuFTEiAcMYx+wsGahvDyVjnq4nmfReDUn1RH6pGKlayPBQW4hUYO0qamV0z8WpYMV0KhUH0tY4Q+ZfSG/AZnBUmTuDTiFsVNcFgbWPIjna8YLfun1H38uvHgVLdnvJN5rbdF9OJQrxbn5RJf1Yw1IDqzZVPfhEbiP5aN1BLUkG9F0MNBQt+UiBxj71rhdCaHno07QoLt1YIgGA7M9BbGC1zM5Bk+4GnwUIt3XNxw69cKixnPmyHNk121Yvcji9TrMIiEEtA6HAqDV4z8U5I6T+hzmyD3GW9Hx5vCDIGAzZzrIn5/eFwhxITVlVdKFBVF7Q7W59IdLSev/viprF7vt74S4JqEtsFlKWDijUpU6z8vo+eS0sZamyBlN7aBEONejXUsGCoYQDjvVvT8GNAZGaEXoqVUvTLH8VS/snCnmvVDQFa/tjR1sdsO3AdwHS/Wz0LgAptnTmJZkGZaq7ZysDyvsxCHxtwLdoxCiPMn2AgewQc2mb2eJMJWAQV0xHzlXZpkA4YWCiNQ5kdCtQ0WuCf4NmscwOmsd/f8/VBD2u3gDKduUmCmahTpm4rAytTrxQyLWW/uGUoKNejMNQ4CTr13cFF8GggenDgpVY89wLr6BHgsQ7DOjidTff5dbI0DnlhAEqSHdjNNh5fiw3Llu75+pC/IB0q2aqoAHcE3XkovZblp881yVJW+VKuPWDcKV36QqU0qJT+dPiN6AIXX7cGHPA4G7COGiF1AFaffDfQsw55KMsCoBok2bqfT7sTz7l+zzcGbdNHc+uVZzZh8jGZwOtzKAOrz12Wpxu8LgkWbr0XUvopEKimGWp4gEUJmAGZl2L1DVgsAYAKhAO19ZJs3o9j+Zkkg8v3XFL/rLaM9frwmbZulj6AppaBNYDOG/CtpApcAmJYuPUnMNS4SNr2gmO2HP/0TDdgMjEBRmDxkjLvOVEs3pWA92hQqDpJtuxrR7k/hl/AFS9f3PQirolAZCBvI9XURxP7/1MGVv/7Lj93upJqLWzh1qWQ0s/pcqdECT6umKGQxwAV05R5j9L8C8A6vMehUAFlBcNhq7oGgNp/wLKTt8bDVatevbDmNT6TIoOtg15MpvyFMTEPoQysPHTigIugTzG5Gowy9OoXPPEdMPTXwykwAUVqxi2KFUwKMgUWKRYYpWhchn/wGGCxAU5BkLWLeQegrATW8cJWVTUB9Q6YqlsqrI4f/P3iEW+zvpNus8Nb3wAVM6sEA25E9wLKwOreHyU8gycf405pwdavwZ3S7ZIAP5+EOyUbUvoUuFxQxeLY76FSjjj+WC+giK6kFa4IWxVVYMr3vQ5ErgzG965+ceZxyqErhXpjUsEAZTqwDCzTE36JzYxx/tZzYFS2DkpzdMHZiRcF7866L5DjFhJE7IhRx8CNfi135COg7EBFdQig4izvJSwdLU80t/7k1SuOamfTCk2h0ruvDKz0HvHDuRGkLtx2OpistXZlw1C7+T2w3xBWwV9yuKpKhh0xApuIYJ+gZDJgVdUGaMQAQD0HfN2w59Wmnxu5kwOoxaBQEfJbRQtlYBWtq/v4IJdyjbx9b1Pzu3uvxXB4QbCmqT4MnqmiBgYL4Mq4aZ7diQleLPokhKM37P5G453mKTrkzcCyi6OuY5KLFpeBVbSu7seDPLKuCffZw5rfjU6OJ2LjIQyFm2S7E2B7MRBI/OmlbwzdbEpXQPWyjmfyFjL+/4JPu45FLkyEAAAAAElFTkSuQmCC'); + background-size: 24px; + background-repeat: no-repeat; + background-position: left center; + padding-left: 36px; + font-size: 20px; + letter-spacing: -0.04rem; + font-weight: 400; + color: white; + text-decoration: none; +} + +.message-container { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + margin: 0 30px; +} + +.message { + font-weight: 300; + font-size: 1.4rem; +} + +body.error .message { + display: none; +} + +body.error .error-message { + display: block; +} + +.error-message { + display: none; + font-weight: 300; + font-size: 1.3rem; +} + +.error-text { + color: red; + font-size: 1rem; +} + +@font-face { + font-family: 'Segoe UI'; + src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.eot"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.eot?#iefix") format("embedded-opentype"); + src: local("Segoe UI Light"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.woff2") format("woff2"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.woff") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.ttf") format("truetype"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/light/latest.svg#web") format("svg"); + font-weight: 200 +} + +@font-face { + font-family: 'Segoe UI'; + src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.eot"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.eot?#iefix") format("embedded-opentype"); + src: local("Segoe UI Semilight"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.woff2") format("woff2"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.woff") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.ttf") format("truetype"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semilight/latest.svg#web") format("svg"); + font-weight: 300 +} + +@font-face { + font-family: 'Segoe UI'; + src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.eot"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.eot?#iefix") format("embedded-opentype"); + src: local("Segoe UI"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff2") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.woff") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.ttf") format("truetype"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/normal/latest.svg#web") format("svg"); + font-weight: 400 +} + +@font-face { + font-family: 'Segoe UI'; + src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.eot"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.eot?#iefix") format("embedded-opentype"); + src: local("Segoe UI Semibold"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.woff2") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.woff") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.ttf") format("truetype"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/semibold/latest.svg#web") format("svg"); + font-weight: 600 +} + +@font-face { + font-family: 'Segoe UI'; + src: url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.eot"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.eot?#iefix") format("embedded-opentype"); + src: local("Segoe UI Bold"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.woff2") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.woff") format("woff"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.ttf") format("truetype"),url("https://c.s-microsoft.com/static/fonts/segoe-ui/west-european/bold/latest.svg#web") format("svg"); + font-weight: 700 +} diff --git a/extensions/github-authentication/media/favicon.ico b/extensions/github-authentication/media/favicon.ico new file mode 100644 index 00000000000..7d1a59f7bda Binary files /dev/null and b/extensions/github-authentication/media/favicon.ico differ diff --git a/extensions/github-authentication/media/icon.png b/extensions/github-authentication/media/icon.png new file mode 100644 index 00000000000..c179f87a711 Binary files /dev/null and b/extensions/github-authentication/media/icon.png differ diff --git a/extensions/github-authentication/media/index.html b/extensions/github-authentication/media/index.html new file mode 100644 index 00000000000..9c0a9eec080 --- /dev/null +++ b/extensions/github-authentication/media/index.html @@ -0,0 +1,37 @@ + + + + + + + Azure Account - Sign In + + + + + + + Visual Studio Code + +
+
+ You are signed in now and can close this page. +
+
+ An error occurred while signing in: +
+
+
+ + + + diff --git a/extensions/github-authentication/src/authServer.ts b/extensions/github-authentication/src/authServer.ts new file mode 100644 index 00000000000..de08c6fca0f --- /dev/null +++ b/extensions/github-authentication/src/authServer.ts @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as http from 'http'; +import { URL } from 'url'; +import * as fs from 'fs'; +import * as path from 'path'; +import { randomBytes } from 'crypto'; + +function sendFile(res: http.ServerResponse, filepath: string) { + fs.readFile(filepath, (err, body) => { + if (err) { + console.error(err); + res.writeHead(404); + res.end(); + } else { + res.writeHead(200, { + 'content-length': body.length, + }); + res.end(body); + } + }); +} + +interface IOAuthResult { + code: string; + state: string; +} + +interface ILoopbackServer { + /** + * If undefined, the server is not started yet. + */ + port: number | undefined; + + /** + * The nonce used + */ + nonce: string; + + /** + * The state parameter used in the OAuth flow. + */ + state: string | undefined; + + /** + * Starts the server. + * @returns The port to listen on. + * @throws If the server fails to start. + * @throws If the server is already started. + */ + start(): Promise; + /** + * Stops the server. + * @throws If the server is not started. + * @throws If the server fails to stop. + */ + stop(): Promise; + /** + * Returns a promise that resolves to the result of the OAuth flow. + */ + waitForOAuthResponse(): Promise; +} + +export class LoopbackAuthServer implements ILoopbackServer { + private readonly _server: http.Server; + private readonly _resultPromise: Promise; + private _startingRedirect: URL; + + public nonce = randomBytes(16).toString('base64'); + public port: number | undefined; + + public set state(state: string | undefined) { + if (state) { + this._startingRedirect.searchParams.set('state', state); + } else { + this._startingRedirect.searchParams.delete('state'); + } + } + public get state(): string | undefined { + return this._startingRedirect.searchParams.get('state') ?? undefined; + } + + constructor(serveRoot: string, startingRedirect: string) { + if (!serveRoot) { + throw new Error('serveRoot must be defined'); + } + if (!startingRedirect) { + throw new Error('startingRedirect must be defined'); + } + this._startingRedirect = new URL(startingRedirect); + let deferred: { resolve: (result: IOAuthResult) => void; reject: (reason: any) => void }; + this._resultPromise = new Promise((resolve, reject) => deferred = { resolve, reject }); + + this._server = http.createServer((req, res) => { + const reqUrl = new URL(req.url!, `http://${req.headers.host}`); + switch (reqUrl.pathname) { + case '/signin': { + const receivedNonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); + if (receivedNonce !== this.nonce) { + res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); + res.end(); + } + res.writeHead(302, { location: this._startingRedirect.toString() }); + res.end(); + break; + } + case '/callback': { + const code = reqUrl.searchParams.get('code') ?? undefined; + const state = reqUrl.searchParams.get('state') ?? undefined; + const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); + if (!code || !state || !nonce) { + res.writeHead(400); + res.end(); + return; + } + if (this.state !== state) { + res.writeHead(302, { location: `/?error=${encodeURIComponent('State does not match.')}` }); + res.end(); + throw new Error('State does not match.'); + } + if (this.nonce !== nonce) { + res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); + res.end(); + throw new Error('Nonce does not match.'); + } + deferred.resolve({ code, state }); + res.writeHead(302, { location: '/' }); + res.end(); + break; + } + // Serve the static files + case '/': + sendFile(res, path.join(serveRoot, 'index.html')); + break; + default: + // substring to get rid of leading '/' + sendFile(res, path.join(serveRoot, reqUrl.pathname.substring(1))); + break; + } + }); + } + + public start(): Promise { + return new Promise((resolve, reject) => { + if (this._server.listening) { + throw new Error('Server is already started'); + } + const portTimeout = setTimeout(() => { + reject(new Error('Timeout waiting for port')); + }, 5000); + this._server.on('listening', () => { + const address = this._server.address(); + if (typeof address === 'string') { + this.port = parseInt(address); + } else if (address instanceof Object) { + this.port = address.port; + } else { + throw new Error('Unable to determine port'); + } + + clearTimeout(portTimeout); + + // set state which will be used to redirect back to vscode + this.state = `http://127.0.0.1:${this.port}/callback?nonce=${encodeURIComponent(this.nonce)}`; + + resolve(this.port); + }); + this._server.on('error', err => { + reject(new Error(`Error listening to server: ${err}`)); + }); + this._server.on('close', () => { + reject(new Error('Closed')); + }); + this._server.listen(0, '127.0.0.1'); + }); + } + + public stop(): Promise { + return new Promise((resolve, reject) => { + if (!this._server.listening) { + throw new Error('Server is not started'); + } + this._server.close((err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + public waitForOAuthResponse(): Promise { + return this._resultPromise; + } +} diff --git a/extensions/github-authentication/src/common/keychain.ts b/extensions/github-authentication/src/common/keychain.ts index 760ae8f3f27..c7b36d96212 100644 --- a/extensions/github-authentication/src/common/keychain.ts +++ b/extensions/github-authentication/src/common/keychain.ts @@ -6,11 +6,8 @@ // keytar depends on a native module shipped in vscode, so this is // how we load it import * as vscode from 'vscode'; -import * as nls from 'vscode-nls'; import { Log } from './logger'; -const localize = nls.loadMessageBundle(); - export class Keychain { constructor( private readonly context: vscode.ExtensionContext, @@ -24,11 +21,6 @@ export class Keychain { } catch (e) { // Ignore this.Logger.error(`Setting token failed: ${e}`); - const troubleshooting = localize('troubleshooting', "Troubleshooting Guide"); - const result = await vscode.window.showErrorMessage(localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", e.message), troubleshooting); - if (result === troubleshooting) { - vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/docs/editor/settings-sync#_troubleshooting-keychain-issues')); - } } } diff --git a/extensions/github-authentication/src/env/browser/authServer.ts b/extensions/github-authentication/src/env/browser/authServer.ts new file mode 100644 index 00000000000..60b53c713a8 --- /dev/null +++ b/extensions/github-authentication/src/env/browser/authServer.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function startServer(_: any): any { + throw new Error('Not implemented'); +} + +export function createServer(_: any): any { + throw new Error('Not implemented'); +} diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 266a8a098d7..9deaef3000b 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -45,10 +45,8 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid if (this.type === AuthProviderType.github) { this._githubServer = new GitHubServer( - // We only can use the Device Code flow when we are running with a remote extension host. - context.extension.extensionKind === vscode.ExtensionKind.Workspace - // This should only matter when we are running in code-oss. See the other change in this commit. - || vscode.env.uiKind === vscode.UIKind.Desktop, + // We only can use the Device Code flow when we have a full node environment because of CORS. + context.extension.extensionKind === vscode.ExtensionKind.Workspace || vscode.env.uiKind === vscode.UIKind.Desktop, this._logger, this._telemetryReporter); } else { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index bcc8ca4e5ea..50f8983d368 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -12,6 +12,8 @@ import { ExperimentationTelemetry } from './experimentationService'; import { AuthProviderType } from './github'; import { Log } from './common/logger'; import { isSupportedEnvironment } from './common/env'; +import { LoopbackAuthServer } from './authServer'; +import path = require('path'); const localize = nls.loadMessageBundle(); const CLIENT_ID = '01ab8ac9400c4e429b23'; @@ -110,15 +112,24 @@ async function getUserInfo(token: string, serverUri: vscode.Uri, logger: Log): P export class GitHubServer implements IGitHubServer { friendlyName = 'GitHub'; type = AuthProviderType.github; - private _onDidManuallyProvideToken = new vscode.EventEmitter(); private _pendingNonces = new Map(); private _codeExchangePromises = new Map; cancel: vscode.EventEmitter }>(); private _disposable: vscode.Disposable; private _uriHandler = new UriEventHandler(this._logger); + private readonly getRedirectEndpoint: Thenable; constructor(private readonly _supportDeviceCodeFlow: boolean, private readonly _logger: Log, private readonly _telemetryReporter: ExperimentationTelemetry) { this._disposable = vscode.window.registerUriHandler(this._uriHandler); + + this.getRedirectEndpoint = vscode.commands.executeCommand<{ [providerId: string]: string } | undefined>('workbench.getCodeExchangeProxyEndpoints').then((proxyEndpoints) => { + // If we are running in insiders vscode.dev, then ensure we use the redirect route on that. + let redirectUri = REDIRECT_URL_STABLE; + if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') { + redirectUri = REDIRECT_URL_INSIDERS; + } + return redirectUri; + }); } dispose() { @@ -134,84 +145,147 @@ export class GitHubServer implements IGitHubServer { public async login(scopes: string): Promise { this._logger.info(`Logging in for the following scopes: ${scopes}`); + // Used for showing a friendlier message to the user when the explicitly cancel a flow. + let userCancelled: boolean = false; + const yes = localize('yes', "Yes"); + const no = localize('no', "No"); + const getMessage = () => userCancelled + ? localize('userCancelledMessage', "Having trouble logging in? Would you like to try a different way?") + : localize('otherReasonMessage', "You have not yet finished authorizing this extension to use GitHub. Would you like to keep trying?"); + const nonce = uuid(); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate?nonce=${encodeURIComponent(nonce)}`)); - if (!isSupportedEnvironment(callbackUri)) { - const token = this._supportDeviceCodeFlow - ? await this.doDeviceCodeFlow(scopes) - : await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); - - if (!token) { throw new Error('No token provided'); } - - const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user'] - const scopesList = scopes.split(' '); // Example: 'read:user repo user:email' - if (!scopesList.every(scope => { - const included = tokenScopes.includes(scope); - if (included || !scope.includes(':')) { - return included; - } - - return scope.split(':').some(splitScopes => { - return tokenScopes.includes(splitScopes); - }); - })) { - throw new Error(`The provided token does not match the requested scopes: ${scopes}`); + const supported = isSupportedEnvironment(callbackUri); + if (supported) { + try { + return await this.doLoginWithoutLocalServer(scopes, nonce, callbackUri); + } catch (e) { + this._logger.error(e); + userCancelled = e.message ?? e === 'User Cancelled'; } - return token; + let choice = await vscode.window.showWarningMessage(getMessage(), yes, no); + if (choice !== yes) { + throw new Error('Cancelled'); + } } - const existingNonces = this._pendingNonces.get(scopes) || []; - this._pendingNonces.set(scopes, [...existingNonces, nonce]); + // Starting a local server isn't supported in web + if (vscode.env.uiKind === vscode.UIKind.Desktop) { + try { + return await this.doLoginWithLocalServer(scopes); + } catch (e) { + this._logger.error(e); + userCancelled = e.message ?? e === 'User Cancelled'; + } - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - // If we are running in insiders vscode.dev, then ensure we use the redirect route on that. - let redirectUri = REDIRECT_URL_STABLE; - if (proxyEndpoints?.github && new URL(proxyEndpoints.github).hostname === 'insiders.vscode.dev') { - redirectUri = REDIRECT_URL_INSIDERS; + let choice = await vscode.window.showWarningMessage(getMessage(), yes, no); + if (choice !== yes) { + throw new Error('Cancelled'); + } } - const searchParams = new URLSearchParams([ - ['client_id', CLIENT_ID], - ['redirect_uri', redirectUri], - ['scope', scopes], - ['state', encodeURIComponent(callbackUri.toString(true))] - ]); - const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`); - return vscode.window.withProgress({ - location: vscode.ProgressLocation.Window, - title: localize('signingIn', " $(mark-github) Signing in to github.com..."), - }, async () => { + if (this._supportDeviceCodeFlow) { + try { + return await this.doLoginDeviceCodeFlow(scopes); + } catch (e) { + this._logger.error(e); + userCancelled = e.message ?? e === 'User Cancelled'; + } + } else { + try { + return await this.doLoginWithPat(scopes); + } catch (e) { + this._logger.error(e); + userCancelled = e.message ?? e === 'User Cancelled'; + } + } + + throw new Error(userCancelled ? 'Cancelled' : 'No auth flow succeeded.'); + } + + private async doLoginWithoutLocalServer(scopes: string, nonce: string, callbackUri: vscode.Uri): Promise { + this._logger.info(`Trying without local server... (${scopes})`); + return await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize('signingIn', "Signing in to github.com..."), + cancellable: true + }, async (_, token) => { + const existingNonces = this._pendingNonces.get(scopes) || []; + this._pendingNonces.set(scopes, [...existingNonces, nonce]); + const redirectUri = await this.getRedirectEndpoint; + const searchParams = new URLSearchParams([ + ['client_id', CLIENT_ID], + ['redirect_uri', redirectUri], + ['scope', scopes], + ['state', encodeURIComponent(callbackUri.toString(true))] + ]); + const uri = vscode.Uri.parse(`${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`); await vscode.env.openExternal(uri); // Register a single listener for the URI callback, in case the user starts the login process multiple times // before completing it. let codeExchangePromise = this._codeExchangePromises.get(scopes); if (!codeExchangePromise) { - codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.exchangeCodeForToken(scopes)); + codeExchangePromise = promiseFromEvent(this._uriHandler.event, this.handleUri(scopes)); this._codeExchangePromises.set(scopes, codeExchangePromise); } - return Promise.race([ - codeExchangePromise.promise, - promiseFromEvent(this._onDidManuallyProvideToken.event, (token: string | undefined, resolve, reject): void => { - if (!token) { - reject('Cancelled'); - } else { - resolve(token); - } - }).promise, - new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)) - ]).finally(() => { + try { + return await Promise.race([ + codeExchangePromise.promise, + new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)), + promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise + ]); + } finally { this._pendingNonces.delete(scopes); codeExchangePromise?.cancel.fire(); this._codeExchangePromises.delete(scopes); - }); + } }); } - private async doDeviceCodeFlow(scopes: string): Promise { + private async doLoginWithLocalServer(scopes: string): Promise { + this._logger.info(`Trying with local server... (${scopes})`); + return await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: localize('signingInAnotherWay', "Signing in to github.com..."), + cancellable: true + }, async (_, token) => { + const redirectUri = await this.getRedirectEndpoint; + const searchParams = new URLSearchParams([ + ['client_id', CLIENT_ID], + ['redirect_uri', redirectUri], + ['scope', scopes], + ]); + const loginUrl = `${GITHUB_AUTHORIZE_URL}?${searchParams.toString()}`; + const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); + const port = await server.start(); + + let codeToExchange; + try { + vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${port}/signin?nonce=${encodeURIComponent(server.nonce)}`)); + const { code } = await Promise.race([ + server.waitForOAuthResponse(), + new Promise((_, reject) => setTimeout(() => reject('Cancelled'), 60000)), + promiseFromEvent(token.onCancellationRequested, (_, __, reject) => { reject('User Cancelled'); }).promise + ]); + codeToExchange = code; + } finally { + setTimeout(() => { + void server.stop(); + }, 5000); + } + + const accessToken = await this.exchangeCodeForToken(codeToExchange); + return accessToken; + }); + } + + private async doLoginDeviceCodeFlow(scopes: string): Promise { + this._logger.info(`Trying device code flow... (${scopes})`); + // Get initial device code const uri = `https://github.com/login/device/code?client_id=${CLIENT_ID}&scope=${scopes}`; const result = await fetch(uri, { @@ -235,7 +309,7 @@ export class GitHubServer implements IGitHubServer { }, 'Copy & Continue to GitHub'); if (modalResult !== 'Copy & Continue to GitHub') { - throw new Error('Cancelled'); + throw new Error('User Cancelled'); } await vscode.env.clipboard.writeText(json.user_code); @@ -243,6 +317,35 @@ export class GitHubServer implements IGitHubServer { const uriToOpen = await vscode.env.asExternalUri(vscode.Uri.parse(json.verification_uri)); await vscode.env.openExternal(uriToOpen); + return await this.waitForDeviceCodeAccessToken(json); + } + + private async doLoginWithPat(scopes: string): Promise { + this._logger.info(`Trying to retrieve PAT... (${scopes})`); + const token = await vscode.window.showInputBox({ prompt: 'GitHub Personal Access Token', ignoreFocusOut: true }); + if (!token) { throw new Error('User Cancelled'); } + + const tokenScopes = await getScopes(token, this.getServerUri('/'), this._logger); // Example: ['repo', 'user'] + const scopesList = scopes.split(' '); // Example: 'read:user repo user:email' + if (!scopesList.every(scope => { + const included = tokenScopes.includes(scope); + if (included || !scope.includes(':')) { + return included; + } + + return scope.split(':').some(splitScopes => { + return tokenScopes.includes(splitScopes); + }); + })) { + throw new Error(`The provided token does not match the requested scopes: ${scopes}`); + } + + return token; + } + + private async waitForDeviceCodeAccessToken( + json: IGitHubDeviceCodeResponse, + ): Promise { return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: true, @@ -252,67 +355,63 @@ export class GitHubServer implements IGitHubServer { json.verification_uri, json.user_code) }, async (_, token) => { - return await this.waitForDeviceCodeAccessToken(json, token); + const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`; + + // Try for 2 minutes + const attempts = 120 / json.interval; + for (let i = 0; i < attempts; i++) { + await new Promise(resolve => setTimeout(resolve, json.interval * 1000)); + if (token.isCancellationRequested) { + throw new Error('User Cancelled'); + } + let accessTokenResult; + try { + accessTokenResult = await fetch(refreshTokenUri, { + method: 'POST', + headers: { + Accept: 'application/json' + } + }); + } catch { + continue; + } + + if (!accessTokenResult.ok) { + continue; + } + + const accessTokenJson = await accessTokenResult.json(); + + if (accessTokenJson.error === 'authorization_pending') { + continue; + } + + if (accessTokenJson.error) { + throw new Error(accessTokenJson.error_description); + } + + return accessTokenJson.access_token; + } + + throw new Error('Cancelled'); }); } - private async waitForDeviceCodeAccessToken( - json: IGitHubDeviceCodeResponse, - token: vscode.CancellationToken - ): Promise { - - const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${CLIENT_ID}&device_code=${json.device_code}&grant_type=urn:ietf:params:oauth:grant-type:device_code`; - - // Try for 2 minutes - const attempts = 120 / json.interval; - for (let i = 0; i < attempts; i++) { - await new Promise(resolve => setTimeout(resolve, json.interval * 1000)); - if (token.isCancellationRequested) { - throw new Error('Cancelled'); - } - let accessTokenResult; - try { - accessTokenResult = await fetch(refreshTokenUri, { - method: 'POST', - headers: { - Accept: 'application/json' - } - }); - } catch { - continue; - } - - if (!accessTokenResult.ok) { - continue; - } - - const accessTokenJson = await accessTokenResult.json(); - - if (accessTokenJson.error === 'authorization_pending') { - continue; - } - - if (accessTokenJson.error) { - throw new Error(accessTokenJson.error_description); - } - - return accessTokenJson.access_token; - } - - throw new Error('Cancelled'); - } - - private exchangeCodeForToken: (scopes: string) => PromiseAdapter = - (scopes) => async (uri, resolve, reject) => { + private handleUri: (scopes: string) => PromiseAdapter = + (scopes) => (uri, resolve, reject) => { const query = new URLSearchParams(uri.query); const code = query.get('code'); - - const acceptedNonces = this._pendingNonces.get(scopes) || []; const nonce = query.get('nonce'); - if (!nonce) { - this._logger.error('No nonce in response.'); + if (!code) { + reject(new Error('No code')); return; } + if (!nonce) { + reject(new Error('No nonce')); + return; + } + + const acceptedNonces = this._pendingNonces.get(scopes) || []; if (!acceptedNonces.includes(nonce)) { // A common scenario of this happening is if you: // 1. Trigger a sign in with one set of scopes @@ -323,36 +422,39 @@ export class GitHubServer implements IGitHubServer { return; } - this._logger.info('Exchanging code for token...'); - - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL; - - try { - const body = `code=${code}`; - const result = await fetch(endpointUrl, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': body.toString() - - }, - body - }); - - if (result.ok) { - const json = await result.json(); - this._logger.info('Token exchange success!'); - resolve(json.access_token); - } else { - reject(result.statusText); - } - } catch (ex) { - reject(ex); - } + resolve(this.exchangeCodeForToken(code)); }; + private async exchangeCodeForToken(code: string): Promise { + this._logger.info('Exchanging code for token...'); + + const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); + const endpointUrl = proxyEndpoints?.github ? `${proxyEndpoints.github}login/oauth/access_token` : GITHUB_TOKEN_URL; + + const body = `code=${code}`; + const result = await fetch(endpointUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': body.toString() + + }, + body + }); + + if (result.ok) { + const json = await result.json(); + this._logger.info('Token exchange success!'); + return json.access_token; + } else { + const text = await result.text(); + const error = new Error(text); + error.name = 'GitHubTokenExchangeError'; + throw error; + } + } + private getServerUri(path: string = '') { const apiUri = vscode.Uri.parse('https://api.github.com'); return vscode.Uri.parse(`${apiUri.scheme}://${apiUri.authority}${path}`); diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 6e0b5fadbf7..c7e40884179 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -18,7 +18,9 @@ function asRemoteSource(raw: any): RemoteSource { const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol'); return { name: `$(github) ${raw.full_name}`, - description: raw.description || undefined, + description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : '' + }`, + detail: raw.description || undefined, url: protocol === 'https' ? raw.clone_url : raw.ssh_url }; } @@ -75,6 +77,8 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider { return []; } + query += ` fork:true`; + const raw = await octokit.search.repos({ q: query, sort: 'stars' }); return raw.data.items.map(asRemoteSource); } diff --git a/extensions/github/src/typings/git-base.d.ts b/extensions/github/src/typings/git-base.d.ts index 70ac3b1b972..8510df6d043 100644 --- a/extensions/github/src/typings/git-base.d.ts +++ b/extensions/github/src/typings/git-base.d.ts @@ -31,9 +31,12 @@ export interface GitBaseExtension { export interface PickRemoteSourceOptions { readonly providerLabel?: (provider: RemoteSourceProvider) => string; - readonly urlLabel?: string; + readonly urlLabel?: string | ((url: string) => string); readonly providerName?: string; + readonly title?: string; + readonly placeholder?: string; readonly branch?: boolean; // then result is PickRemoteSourceResult + readonly showRecentSources?: boolean; } export interface PickRemoteSourceResult { @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult { export interface RemoteSource { readonly name: string; readonly description?: string; + readonly detail?: string; + /** + * Codicon name + */ + readonly icon?: string; readonly url: string | string[]; } +export interface RecentRemoteSource extends RemoteSource { + readonly timestamp: number; +} + export interface RemoteSourceProvider { readonly name: string; /** * Codicon name */ readonly icon?: string; + readonly label?: string; + readonly placeholder?: string; readonly supportsQuery?: boolean; getBranches?(url: string): ProviderResult; + getRecentRemoteSources?(query?: string): ProviderResult; getRemoteSources(query?: string): ProviderResult; } diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index b0df59821b8..6713556ae38 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -67,7 +67,7 @@ namespace SemanticTokenLegendRequest { export interface RuntimeEnvironment { fileFs?: FileSystemProvider; - configureHttpRequests?(proxy: string, strictSSL: boolean): void; + configureHttpRequests?(proxy: string | undefined, strictSSL: boolean): void; readonly timer: { setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable; setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index 414bedf2c52..c8feabc17af 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -10,8 +10,7 @@ }, "enabledApiProposals": [ "notebookEditor", - "notebookEditorEdit", - "notebookDocumentEvents" + "notebookEditorEdit" ], "activationEvents": [ "*" diff --git a/extensions/ipynb/tsconfig.json b/extensions/ipynb/tsconfig.json index b437665bd92..178e86493b4 100644 --- a/extensions/ipynb/tsconfig.json +++ b/extensions/ipynb/tsconfig.json @@ -10,7 +10,6 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.notebookEditor.d.ts", - "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts", - "../../src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts", + "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts" ] } diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 1044fa22599..9bf63477ebe 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -59,7 +59,8 @@ namespace ResultLimitReachedNotification { interface Settings { json?: { schemas?: JSONSchemaSettings[]; - format?: { enable: boolean }; + format?: { enable?: boolean }; + validate?: { enable?: boolean }; resultLimit?: number; }; http?: { @@ -76,6 +77,7 @@ export interface JSONSchemaSettings { namespace SettingIds { export const enableFormatter = 'json.format.enable'; + export const enableValidation = 'json.validate.enable'; export const enableSchemaDownload = 'json.schemaDownload.enable'; export const maxItemsComputed = 'json.maxItemsComputed'; } @@ -425,6 +427,7 @@ function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] } function getSettings(): Settings { + const configuration = workspace.getConfiguration(); const httpSettings = workspace.getConfiguration('http'); const resultLimit: number = Math.trunc(Math.max(0, Number(workspace.getConfiguration().get(SettingIds.maxItemsComputed)))) || 5000; @@ -435,6 +438,8 @@ function getSettings(): Settings { proxyStrictSSL: httpSettings.get('proxyStrictSSL') }, json: { + validate: { enable: configuration.get(SettingIds.enableValidation) }, + format: { enable: configuration.get(SettingIds.enableFormatter) }, schemas: [], resultLimit } diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index d82ebaa85f5..25aef750a76 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -73,6 +73,12 @@ } } }, + "json.validate.enable": { + "type": "boolean", + "scope": "window", + "default": true, + "description": "%json.validate.enable.desc%" + }, "json.format.enable": { "type": "boolean", "scope": "window", @@ -141,8 +147,8 @@ ] }, "dependencies": { - "request-light": "^0.5.7", - "@vscode/extension-telemetry": "0.4.10", + "@vscode/extension-telemetry": "0.5.0", + "request-light": "^0.5.8", "vscode-languageclient": "^7.0.0", "vscode-nls": "^5.0.0" }, diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index f83dd588ecd..8afec56a90f 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -7,6 +7,7 @@ "json.schemas.fileMatch.item.desc": "A file pattern that can contain '*' to match against when resolving JSON files to schemas.", "json.schemas.schema.desc": "The schema definition for the given URL. The schema only needs to be provided to avoid accesses to the schema URL.", "json.format.enable.desc": "Enable/disable default JSON formatter", + "json.validate.enable.desc": "Enable/disable JSON validation.", "json.tracing.desc": "Traces the communication between VS Code and the JSON language server.", "json.colorDecorators.enable.desc": "Enables or disables color decorators", "json.colorDecorators.enable.deprecationMessage": "The setting `json.colorDecorators.enable` has been deprecated in favor of `editor.colorDecorators`.", diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index 328a523f5a2..e82ae06d776 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -62,6 +62,8 @@ The server supports the following settings: - json - `format` - `enable`: Whether the server should register the formatting support. This option is only applicable if the client supports *dynamicRegistration* for *rangeFormatting* and `initializationOptions.provideFormatter` is not defined. + - `validate` + - `enable`: Whether the server should validate. Defaults to `true` if not set. - `schemas`: Configures association of file names to schema URL or schemas and/or associations of schema URL to schema content. - `fileMatch`: an array of file names or paths (separated by `/`). `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there is at least one matching pattern and the last matching pattern is not an exclusion pattern. - `url`: The URL of the schema, optional when also a schema is provided. diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index cb3fd6c2c78..cdb3d80ddd9 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -13,7 +13,7 @@ "main": "./out/node/jsonServerMain", "dependencies": { "jsonc-parser": "^3.0.0", - "request-light": "^0.5.7", + "request-light": "^0.5.8", "vscode-json-languageservice": "^4.2.1", "vscode-languageserver": "^7.0.0", "vscode-uri": "^3.0.3" diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index a44baf31f30..e105859371f 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -57,7 +57,7 @@ export interface RequestService { export interface RuntimeEnvironment { file?: RequestService; http?: RequestService; - configureHttpRequests?(proxy: string, strictSSL: boolean): void; + configureHttpRequests?(proxy: string | undefined, strictSSL: boolean): void; readonly timer: { setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable; setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; @@ -166,14 +166,15 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) // The settings interface describes the server relevant settings part interface Settings { - json: { - schemas: JSONSchemaSettings[]; - format: { enable: boolean }; + json?: { + schemas?: JSONSchemaSettings[]; + format?: { enable?: boolean }; + validate?: { enable?: boolean }; resultLimit?: number; }; - http: { - proxy: string; - proxyStrictSSL: boolean; + http?: { + proxy?: string; + proxyStrictSSL?: boolean; }; } @@ -226,22 +227,24 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined; let schemaAssociations: ISchemaAssociations | SchemaConfiguration[] | undefined = undefined; let formatterRegistrations: Thenable[] | null = null; + let validateEnabled = true; // The settings have changed. Is send on server activation as well. connection.onDidChangeConfiguration((change) => { let settings = change.settings; if (runtime.configureHttpRequests) { - runtime.configureHttpRequests(settings.http && settings.http.proxy, settings.http && settings.http.proxyStrictSSL); + runtime.configureHttpRequests(settings?.http?.proxy, !!settings.http?.proxyStrictSSL); } - jsonConfigurationSettings = settings.json && settings.json.schemas; + jsonConfigurationSettings = settings.json?.schemas; + validateEnabled = !!settings.json?.validate?.enable; updateConfiguration(); - foldingRangeLimit = Math.trunc(Math.max(settings.json && settings.json.resultLimit || foldingRangeLimitDefault, 0)); - resultLimit = Math.trunc(Math.max(settings.json && settings.json.resultLimit || Number.MAX_VALUE, 0)); + foldingRangeLimit = Math.trunc(Math.max(settings.json?.resultLimit || foldingRangeLimitDefault, 0)); + resultLimit = Math.trunc(Math.max(settings.json?.resultLimit || Number.MAX_VALUE, 0)); // dynamically enable & disable the formatter if (dynamicFormatterRegistration) { - const enableFormatter = settings && settings.json && settings.json.format && settings.json.format.enable; + const enableFormatter = settings.json?.format?.enable; if (enableFormatter) { if (!formatterRegistrations) { const documentSelector = [{ language: 'json' }, { language: 'jsonc' }]; @@ -309,7 +312,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) function updateConfiguration() { const languageSettings = { - validate: true, + validate: validateEnabled, allowComments: true, schemas: new Array() }; @@ -371,10 +374,14 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) function triggerValidation(textDocument: TextDocument): void { cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); + if (validateEnabled) { + pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(() => { + delete pendingValidationRequests[textDocument.uri]; + validateTextDocument(textDocument); + }, validationDelayMs); + } else { + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] }); + } } function validateTextDocument(textDocument: TextDocument, callback?: (diagnostics: Diagnostic[]) => void): void { diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 483e6cc060a..b43c8aa2131 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -17,10 +17,10 @@ jsonc-parser@^3.0.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== -request-light@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.7.tgz#1c448c22153b55d2cd278eb414df24a5ad6e6d5e" - integrity sha512-i/wKzvcx7Er8tZnvqSxWuNO5ZGggu2UgZAqj/RyZ0si7lBTXL7kZiI/dWxzxnQjaY7s5HEy1qK21Do4Ncr6cVw== +request-light@^0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.8.tgz#8bf73a07242b9e7b601fac2fa5dc22a094abcc27" + integrity sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg== vscode-json-languageservice@^4.2.1: version "4.2.1" diff --git a/extensions/json-language-features/yarn.lock b/extensions/json-language-features/yarn.lock index 2049e0f32bf..98216da36e6 100644 --- a/extensions/json-language-features/yarn.lock +++ b/extensions/json-language-features/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@vscode/extension-telemetry@0.4.10": - version "0.4.10" - resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.10.tgz#be960c05bdcbea0933866346cf244acad6cac910" - integrity sha512-XgyUoWWRQExTmd9DynIIUQo1NPex/zIeetdUAXeBjVuW9ioojM1TcDaSqOa/5QLC7lx+oEXwSU1r0XSBgzyz6w== +"@vscode/extension-telemetry@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.5.0.tgz#8214171e550393d577fc56326fa986c6800b831b" + integrity sha512-27FsgeVJvC4zVw7Ar3Ub+7vJswDt8RoBFpbgBwf8Xq/B2gaT8G6a+gkw3s2pQmjWGIqyu7TRA8e9rS8/vxv6NQ== balanced-match@^1.0.0: version "1.0.0" @@ -44,10 +44,10 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -request-light@^0.5.7: - version "0.5.7" - resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.7.tgz#1c448c22153b55d2cd278eb414df24a5ad6e6d5e" - integrity sha512-i/wKzvcx7Er8tZnvqSxWuNO5ZGggu2UgZAqj/RyZ0si7lBTXL7kZiI/dWxzxnQjaY7s5HEy1qK21Do4Ncr6cVw== +request-light@^0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.5.8.tgz#8bf73a07242b9e7b601fac2fa5dc22a094abcc27" + integrity sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg== semver@^7.3.4: version "7.3.4" diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 933fdc4d314..06f3fb5a14b 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "a8545b220dc2cb109835571ba3458ff138e97361" + "commitHash": "df4829558048663156abcc4235619c773335445e" } }, "license": "MIT", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index e91e0f6ea60..fa453cecac4 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/a8545b220dc2cb109835571ba3458ff138e97361", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/df4829558048663156abcc4235619c773335445e", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -2918,7 +2918,7 @@ "name": "punctuation.definition.strikethrough.markdown" } }, - "match": "(~+)((?:[^~]|(?!(? { const mdPath = document.uri.scheme === uri.scheme - ? path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath) + ? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath)) : uri.toString(false); const ext = URI.Utils.extname(uri).toLowerCase(); diff --git a/extensions/markdown-language-features/src/languageFeatures/references.ts b/extensions/markdown-language-features/src/languageFeatures/references.ts index 2cc23912445..b60bb447c7f 100644 --- a/extensions/markdown-language-features/src/languageFeatures/references.ts +++ b/extensions/markdown-language-features/src/languageFeatures/references.ts @@ -136,11 +136,11 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference if (link.ref.range.contains(position)) { return Array.from(this.getReferencesToLinkReference(docLinks, link.ref.text, { resource: document.uri, range: link.ref.range })); } else if (link.source.hrefRange.contains(position)) { - return this.getReferencesToLink(link, token); + return this.getReferencesToLink(link, position, token); } } else { if (link.source.hrefRange.contains(position)) { - return this.getReferencesToLink(link, token); + return this.getReferencesToLink(link, position, token); } } } @@ -148,7 +148,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference return []; } - private async getReferencesToLink(sourceLink: MdLink, token: vscode.CancellationToken): Promise { + private async getReferencesToLink(sourceLink: MdLink, triggerPosition: vscode.Position, token: vscode.CancellationToken): Promise { const allLinksInWorkspace = (await this._linkCache.getAll()).flat(); if (token.isCancellationRequested) { return []; @@ -176,22 +176,14 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference return references; } - let targetDoc = await this.workspaceContents.getMarkdownDocument(sourceLink.href.path); - if (!targetDoc) { - // We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead - if (uri.Utils.extname(sourceLink.href.path) === '') { - const dotMdResource = sourceLink.href.path.with({ path: sourceLink.href.path.path + '.md' }); - targetDoc = await this.workspaceContents.getMarkdownDocument(dotMdResource); - } - } - - if (!targetDoc || token.isCancellationRequested) { + const targetDoc = await tryFindMdDocumentForLink(sourceLink.href, this.workspaceContents); + if (token.isCancellationRequested) { return []; } const references: MdReference[] = []; - if (sourceLink.href.fragment) { + if (targetDoc && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) { const toc = await TableOfContents.create(this.engine, targetDoc); const entry = toc.lookup(sourceLink.href.fragment); if (entry) { @@ -222,7 +214,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference } } } else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments - references.push(...this.findAllLinksToFile(targetDoc.uri, allLinksInWorkspace, sourceLink)); + references.push(...this.findAllLinksToFile(targetDoc?.uri ?? sourceLink.href.path, allLinksInWorkspace, sourceLink)); } return references; @@ -250,12 +242,13 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference } const isTriggerLocation = !!sourceLink && sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange); + const pathRange = this.getPathRange(link); yield { kind: 'link', isTriggerLocation, isDefinition: false, link, - location: new vscode.Location(link.source.resource, link.source.hrefRange), + location: new vscode.Location(link.source.resource, pathRange), }; } } @@ -274,14 +267,41 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference if (ref === refToFind && link.source.resource.fsPath === from.resource.fsPath) { const isTriggerLocation = from.resource.fsPath === link.source.resource.fsPath && ( (link.href.kind === 'reference' && from.range.isEqual(link.source.hrefRange)) || (link.kind === 'definition' && from.range.isEqual(link.ref.range))); + + const pathRange = this.getPathRange(link); yield { kind: 'link', isTriggerLocation, isDefinition: link.kind === 'definition', link, - location: new vscode.Location(from.resource, link.source.hrefRange), + location: new vscode.Location(from.resource, pathRange), }; } } } + + /** + * Get just the range of the file path, dropping the fragment + */ + private getPathRange(link: MdLink): vscode.Range { + return link.source.fragmentRange + ? link.source.hrefRange.with(undefined, link.source.fragmentRange.start.translate(0, -1)) + : link.source.hrefRange; + } } + +export async function tryFindMdDocumentForLink(href: InternalHref, workspaceContents: MdWorkspaceContents): Promise { + const targetDoc = await workspaceContents.getMarkdownDocument(href.path); + if (targetDoc) { + return targetDoc; + } + + // We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead + if (uri.Utils.extname(href.path) === '') { + const dotMdResource = href.path.with({ path: href.path.path + '.md' }); + return workspaceContents.getMarkdownDocument(dotMdResource); + } + + return undefined; +} + diff --git a/extensions/markdown-language-features/src/languageFeatures/rename.ts b/extensions/markdown-language-features/src/languageFeatures/rename.ts index 51379366730..4184741a411 100644 --- a/extensions/markdown-language-features/src/languageFeatures/rename.ts +++ b/extensions/markdown-language-features/src/languageFeatures/rename.ts @@ -2,16 +2,49 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import * as URI from 'vscode-uri'; import { Slugifier } from '../slugify'; import { Disposable } from '../util/dispose'; -import { SkinnyTextDocument } from '../workspaceContents'; -import { MdHeaderReference, MdReference, MdReferencesProvider } from './references'; +import { resolveDocumentLink } from '../util/openDocumentLink'; +import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents'; +import { InternalHref } from './documentLinkProvider'; +import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references'; const localize = nls.loadMessageBundle(); +export interface MdReferencesResponse { + references: MdReference[]; + triggerRef: MdReference; +} + +interface MdFileRenameEdit { + readonly from: vscode.Uri; + readonly to: vscode.Uri; +} + +/** + * Type with additional metadata about the edits for testing + * + * This is needed since `vscode.WorkspaceEdit` does not expose info on file renames. + */ +export interface MdWorkspaceEdit { + readonly edit: vscode.WorkspaceEdit; + + readonly fileRenames?: ReadonlyArray; +} + +function tryDecodeUri(str: string): string { + try { + return decodeURI(str); + } catch { + return str; + } +} + export class MdRenameProvider extends Disposable implements vscode.RenameProvider { private cachedRefs?: { @@ -22,8 +55,11 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide readonly references: MdReference[]; } | undefined; + private readonly renameNotSupportedText = localize('invalidRenameLocation', "Rename not supported at location"); + public constructor( private readonly referencesProvider: MdReferencesProvider, + private readonly workspaceContents: MdWorkspaceContents, private readonly slugifier: Slugifier, ) { super(); @@ -36,7 +72,7 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide } if (!allRefsInfo || !allRefsInfo.references.length) { - throw new Error(localize('invalidRenameLocation', "Rename not supported at location")); + throw new Error(this.renameNotSupportedText); } const triggerRef = allRefsInfo.triggerRef; @@ -56,8 +92,9 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) }; } + // See if we are renaming the fragment or the path const { fragmentRange } = triggerRef.link.source; - if (fragmentRange) { + if (fragmentRange?.contains(position)) { const declaration = this.findHeaderDeclaration(allRefsInfo.references); if (declaration) { return { range: fragmentRange, placeholder: declaration.headerText }; @@ -65,16 +102,31 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide return { range: fragmentRange, placeholder: document.getText(fragmentRange) }; } - throw new Error(localize('renameNoFiles', "Renaming files is currently not supported")); + const range = this.getFilePathRange(triggerRef); + if (!range) { + throw new Error(this.renameNotSupportedText); + } + return { range, placeholder: tryDecodeUri(document.getText(range)) }; } } } + private getFilePathRange(ref: MdLinkReference): vscode.Range { + if (ref.link.source.fragmentRange) { + return ref.link.source.hrefRange.with(undefined, ref.link.source.fragmentRange.start.translate(0, -1)); + } + return ref.link.source.hrefRange; + } + private findHeaderDeclaration(references: readonly MdReference[]): MdHeaderReference | undefined { return references.find(ref => ref.isDefinition && ref.kind === 'header') as MdHeaderReference | undefined; } public async provideRenameEdits(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { + return (await this.provideRenameEditsImpl(document, position, newName, token))?.edit; + } + + public async provideRenameEditsImpl(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise { const allRefsInfo = await this.getAllReferences(document, position, token); if (token.isCancellationRequested || !allRefsInfo || !allRefsInfo.references.length) { return undefined; @@ -82,9 +134,66 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide const triggerRef = allRefsInfo.triggerRef; - const isRefRename = triggerRef.kind === 'link' && ( + if (triggerRef.kind === 'link' && ( (triggerRef.link.kind === 'definition' && triggerRef.link.ref.range.contains(position)) || triggerRef.link.href.kind === 'reference' - ); + )) { + return this.renameReferenceLinks(allRefsInfo, newName); + } else if (triggerRef.kind === 'link' && triggerRef.link.href.kind === 'external') { + return this.renameExternalLink(allRefsInfo, newName); + } else if (triggerRef.kind === 'header' || (triggerRef.kind === 'link' && triggerRef.link.source.fragmentRange?.contains(position) && (triggerRef.link.kind === 'definition' || triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal'))) { + return this.renameFragment(allRefsInfo, newName); + } else if (triggerRef.kind === 'link' && !triggerRef.link.source.fragmentRange?.contains(position) && triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal') { + return this.renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName); + } + + return undefined; + } + + private async renameFilePath(triggerDocument: vscode.Uri, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string): Promise { + const edit = new vscode.WorkspaceEdit(); + const fileRenames: MdFileRenameEdit[] = []; + + const targetDoc = await tryFindMdDocumentForLink(triggerHref, this.workspaceContents); + const targetUri = targetDoc?.uri ?? triggerHref.path; + + const rawNewFilePath = resolveDocumentLink(newName, triggerDocument); + let resolvedNewFilePath = rawNewFilePath; + if (!URI.Utils.extname(resolvedNewFilePath)) { + // If the newly entered path doesn't have a file extension but the original file did + // tack on a .md file extension + if (URI.Utils.extname(targetUri)) { + resolvedNewFilePath = resolvedNewFilePath.with({ + path: resolvedNewFilePath.path + '.md' + }); + } + } + + // First rename the file + fileRenames.push({ from: targetUri, to: resolvedNewFilePath }); + edit.renameFile(targetUri, resolvedNewFilePath); + + // Then update all refs to it + for (const ref of allRefsInfo.references) { + if (ref.kind === 'link') { + // Try to preserve style of existing links + let newPath: string; + if (ref.link.source.text.startsWith('/')) { + const root = resolveDocumentLink('/', ref.link.source.resource); + newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true)); + } else { + newPath = path.relative(URI.Utils.dirname(ref.link.source.resource).toString(true), rawNewFilePath.toString(true)); + if (newName.startsWith('./') && !newPath.startsWith('../')) { + newPath = './' + newPath; + } + } + edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newPath)); + } + } + + return { edit, fileRenames }; + } + + private renameFragment(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit { const slug = this.slugifier.fromHeading(newName).value; const edit = new vscode.WorkspaceEdit(); @@ -95,22 +204,38 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide break; case 'link': - if (ref.link.kind === 'definition') { - // We may be renaming either the reference or the definition itself - if (isRefRename) { - edit.replace(ref.link.source.resource, ref.link.ref.range, newName); - continue; - } - } - edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, isRefRename && !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug); + edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug); break; } } - - return edit; + return { edit }; } - private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<{ references: MdReference[]; triggerRef: MdReference } | undefined> { + private renameExternalLink(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit { + const edit = new vscode.WorkspaceEdit(); + for (const ref of allRefsInfo.references) { + if (ref.kind === 'link') { + edit.replace(ref.link.source.resource, ref.location.range, newName); + } + } + return { edit }; + } + + private renameReferenceLinks(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit { + const edit = new vscode.WorkspaceEdit(); + for (const ref of allRefsInfo.references) { + if (ref.kind === 'link') { + if (ref.link.kind === 'definition') { + edit.replace(ref.link.source.resource, ref.link.ref.range, newName); + } else { + edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName); + } + } + } + return { edit }; + } + + private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { const version = document.version; if (this.cachedRefs diff --git a/extensions/markdown-language-features/src/test/references.test.ts b/extensions/markdown-language-features/src/test/references.test.ts index 1a1c5f444b2..287612a2154 100644 --- a/extensions/markdown-language-features/src/test/references.test.ts +++ b/extensions/markdown-language-features/src/test/references.test.ts @@ -23,7 +23,7 @@ function getReferences(doc: InMemoryDocument, pos: vscode.Position, workspaceCon return provider.provideReferences(doc, pos, { includeDeclaration: true }, noopToken); } -function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expectedRefs: { uri: vscode.Uri; line: number }[]) { +function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expectedRefs: { uri: vscode.Uri; line: number; startCharacter?: number; endCharacter?: number }[]) { assert.strictEqual(actualRefs.length, expectedRefs.length, `Reference counts should match`); for (let i = 0; i < actualRefs.length; ++i) { @@ -32,6 +32,12 @@ function assertReferencesEqual(actualRefs: readonly vscode.Location[], ...expect assert.strictEqual(actual.uri.toString(), expected.uri.toString(), `Ref '${i}' has expected document`); assert.strictEqual(actual.range.start.line, expected.line, `Ref '${i}' has expected start line`); assert.strictEqual(actual.range.end.line, expected.line, `Ref '${i}' has expected end line`); + if (typeof expected.startCharacter !== 'undefined') { + assert.strictEqual(actual.range.start.character, expected.startCharacter, `Ref '${i}' has expected start character`); + } + if (typeof expected.endCharacter !== 'undefined') { + assert.strictEqual(actual.range.end.character, expected.endCharacter, `Ref '${i}' has expected end character`); + } } } @@ -158,7 +164,7 @@ suite('markdown: find all references', () => { ); }); - test('Should find references from link definition', async () => { + test('Should find header references from link definition', async () => { const uri = workspacePath('doc.md'); const doc = new InMemoryDocument(uri, joinLines( `# A b C`, @@ -265,7 +271,7 @@ suite('markdown: find all references', () => { `[without ext](./sub/other.md#header)`, )); - const refs = await getReferences(doc, new vscode.Position(0, 15), new InMemoryWorkspaceMarkdownDocuments([ + const refs = await getReferences(doc, new vscode.Position(0, 23), new InMemoryWorkspaceMarkdownDocuments([ doc, new InMemoryDocument(other1Uri, joinLines( `pre`, @@ -415,6 +421,73 @@ suite('markdown: find all references', () => { ); }); + test('Should distinguish between references to file and to header within file', async () => { + const docUri = workspacePath('doc.md'); + const other1Uri = workspacePath('sub', 'other.md'); + + const doc = new InMemoryDocument(docUri, joinLines( + `# abc`, + ``, + `[link 1](#abc)`, + )); + const otherDoc = new InMemoryDocument(other1Uri, joinLines( + `[link](/doc.md#abc)`, + `[link no text](/doc#abc)`, + )); + const workspaceContents = new InMemoryWorkspaceMarkdownDocuments([ + doc, + otherDoc, + ]); + { + // Check refs to header fragment + const headerRefs = await getReferences(otherDoc, new vscode.Position(0, 16), workspaceContents); + assertReferencesEqual(headerRefs!, + { uri: docUri, line: 0 }, // Header definition + { uri: docUri, line: 2 }, + { uri: other1Uri, line: 0 }, + { uri: other1Uri, line: 1 }, + ); + } + { + // Check refs to file itself from link with ext + const fileRefs = await getReferences(otherDoc, new vscode.Position(0, 9), workspaceContents); + assertReferencesEqual(fileRefs!, + { uri: other1Uri, line: 0, endCharacter: 14 }, + { uri: other1Uri, line: 1, endCharacter: 19 }, + ); + } + { + // Check refs to file itself from link without ext + const fileRefs = await getReferences(otherDoc, new vscode.Position(1, 17), workspaceContents); + assertReferencesEqual(fileRefs!, + { uri: other1Uri, line: 0 }, + { uri: other1Uri, line: 1 }, + ); + } + }); + + test('Should support finding references to unknown file', async () => { + const uri1 = workspacePath('doc1.md'); + const doc1 = new InMemoryDocument(uri1, joinLines( + `![img](/images/more/image.png)`, + ``, + `[ref]: /images/more/image.png`, + )); + + const uri2 = workspacePath('sub', 'doc2.md'); + const doc2 = new InMemoryDocument(uri2, joinLines( + `![img](/images/more/image.png)`, + )); + + + const refs = await getReferences(doc1, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc1, doc2])); + assertReferencesEqual(refs!, + { uri: uri1, line: 0 }, + { uri: uri1, line: 2 }, + { uri: uri2, line: 0 }, + ); + }); + suite('Reference links', () => { test('Should find reference links within file from link', async () => { const docUri = workspacePath('doc.md'); diff --git a/extensions/markdown-language-features/src/test/rename.test.ts b/extensions/markdown-language-features/src/test/rename.test.ts index 63aabb2d320..aedad07d917 100644 --- a/extensions/markdown-language-features/src/test/rename.test.ts +++ b/extensions/markdown-language-features/src/test/rename.test.ts @@ -8,7 +8,7 @@ import 'mocha'; import * as vscode from 'vscode'; import { MdLinkProvider } from '../languageFeatures/documentLinkProvider'; import { MdReferencesProvider } from '../languageFeatures/references'; -import { MdRenameProvider } from '../languageFeatures/rename'; +import { MdRenameProvider, MdWorkspaceEdit } from '../languageFeatures/rename'; import { githubSlugifier } from '../slugify'; import { InMemoryDocument } from '../util/inMemoryDocument'; import { MdWorkspaceContents } from '../workspaceContents'; @@ -24,42 +24,67 @@ function prepareRename(doc: InMemoryDocument, pos: vscode.Position, workspaceCon const engine = createNewMarkdownEngine(); const linkProvider = new MdLinkProvider(engine); const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier); - const renameProvider = new MdRenameProvider(referencesProvider, githubSlugifier); + const renameProvider = new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier); return renameProvider.prepareRename(doc, pos, noopToken); } /** * Get all the edits for the rename. */ -function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspaceContents: MdWorkspaceContents) { +function getRenameEdits(doc: InMemoryDocument, pos: vscode.Position, newName: string, workspaceContents: MdWorkspaceContents): Promise { const engine = createNewMarkdownEngine(); const linkProvider = new MdLinkProvider(engine); const referencesProvider = new MdReferencesProvider(linkProvider, workspaceContents, engine, githubSlugifier); - const renameProvider = new MdRenameProvider(referencesProvider, githubSlugifier); - return renameProvider.provideRenameEdits(doc, pos, newName, noopToken); + const renameProvider = new MdRenameProvider(referencesProvider, workspaceContents, githubSlugifier); + return renameProvider.provideRenameEditsImpl(doc, pos, newName, noopToken); } -function assertEditsEqual(actualEdit: vscode.WorkspaceEdit, ...expectedEdits: { uri: vscode.Uri; edits: vscode.TextEdit[] }[]) { - const actualEntries = actualEdit.entries(); - assert.strictEqual(actualEntries.length, expectedEdits.length, `Reference counts should match`); +interface ExpectedTextEdit { + readonly uri: vscode.Uri; + readonly edits: readonly vscode.TextEdit[]; +} - for (let i = 0; i < actualEntries.length; ++i) { - const actual = actualEntries[i]; - const expected = expectedEdits[i]; - assert.strictEqual(actual[0].toString(), expected.uri.toString(), `Ref '${i}' has expected document`); +interface ExpectedFileRename { + readonly originalUri: vscode.Uri; + readonly newUri: vscode.Uri; +} - const actualEditForDoc = actual[1]; - const expectedEditsForDoc = expected.edits; - assert.strictEqual(actualEditForDoc.length, expectedEditsForDoc.length, `Edit counts for '${actual[0]}' should match`); +function assertEditsEqual(actualEdit: MdWorkspaceEdit, ...expectedEdits: ReadonlyArray) { + // Check file renames + const expectedFileRenames = expectedEdits.filter(expected => 'originalUri' in expected) as ExpectedFileRename[]; + const actualFileRenames = actualEdit.fileRenames ?? []; + assert.strictEqual(actualFileRenames.length, expectedFileRenames.length, `File rename count should match`); + for (let i = 0; i < actualFileRenames.length; ++i) { + const expected = expectedFileRenames[i]; + const actual = actualFileRenames[i]; + assert.strictEqual(actual.from.toString(), expected.originalUri.toString(), `File rename '${i}' should have expected 'from' resource`); + assert.strictEqual(actual.to.toString(), expected.newUri.toString(), `File rename '${i}' should have expected 'to' resource`); + } - for (let g = 0; g < actualEditForDoc.length; ++g) { - assertRangeEqual(actualEditForDoc[g].range, expectedEditsForDoc[g].range, `Edit '${g}' of '${actual[0]}' has expected expected range. Expected range: ${JSON.stringify(actualEditForDoc[g].range)}. Actual range: ${JSON.stringify(expectedEditsForDoc[g].range)}`); - assert.strictEqual(actualEditForDoc[g].newText, expectedEditsForDoc[g].newText, `Edit '${g}' of '${actual[0]}' has expected edits`); + // Check text edits + const actualTextEdits = actualEdit.edit.entries(); + const expectedTextEdits = expectedEdits.filter(expected => 'edits' in expected) as ExpectedTextEdit[]; + assert.strictEqual(actualTextEdits.length, expectedTextEdits.length, `Reference counts should match`); + for (let i = 0; i < actualTextEdits.length; ++i) { + const expected = expectedTextEdits[i]; + const actual = actualTextEdits[i]; + + if ('edits' in expected) { + assert.strictEqual(actual[0].toString(), expected.uri.toString(), `Ref '${i}' has expected document`); + + const actualEditForDoc = actual[1]; + const expectedEditsForDoc = expected.edits; + assert.strictEqual(actualEditForDoc.length, expectedEditsForDoc.length, `Edit counts for '${actual[0]}' should match`); + + for (let g = 0; g < actualEditForDoc.length; ++g) { + assertRangeEqual(actualEditForDoc[g].range, expectedEditsForDoc[g].range, `Edit '${g}' of '${actual[0]}' has expected expected range. Expected range: ${JSON.stringify(actualEditForDoc[g].range)}. Actual range: ${JSON.stringify(expectedEditsForDoc[g].range)}`); + assert.strictEqual(actualEditForDoc[g].newText, expectedEditsForDoc[g].newText, `Edit '${g}' of '${actual[0]}' has expected edits`); + } } } } -suite('markdown: rename', () => { +suite.skip('markdown: rename', () => { // TODO@mjbvz https://github.com/microsoft/vscode/issues/c setup(async () => { // the tests make the assumption that link providers are already registered @@ -325,24 +350,224 @@ suite('markdown: rename', () => { await assert.rejects(prepareRename(doc, new vscode.Position(1, 2), new InMemoryWorkspaceMarkdownDocuments([doc]))); }); - test('Rename should not be supported on bare file link', async () => { - const uri = workspacePath('doc.md'); - const doc = new InMemoryDocument(uri, joinLines( - `[text](./doc.md)`, - `[other](./doc.md)`, - )); - - await assert.rejects(prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc]))); - }); - - test('Rename should not be supported on bare file link in definition', async () => { + test('Path rename should use file path as range', async () => { const uri = workspacePath('doc.md'); const doc = new InMemoryDocument(uri, joinLines( `[text](./doc.md)`, `[ref]: ./doc.md`, )); - await assert.rejects(prepareRename(doc, new vscode.Position(1, 10), new InMemoryWorkspaceMarkdownDocuments([doc]))); + const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc])); + assert.strictEqual(info!.placeholder, './doc.md'); + assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15)); + }); + + test('Path rename\'s range should excludes fragment', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](./doc.md#some-header)`, + `[ref]: ./doc.md#some-header`, + )); + + const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc])); + assert.strictEqual(info!.placeholder, './doc.md'); + assertRangeEqual(info!.range, new vscode.Range(0, 7, 0, 15)); + }); + + test('Path rename should update file and all refs', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](./doc.md)`, + `[ref]: ./doc.md`, + )); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 10), './sub/newDoc.md', new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + originalUri: uri, + newUri: workspacePath('sub', 'newDoc.md'), + }, { + uri: uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './sub/newDoc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './sub/newDoc.md'), + ] + }); + }); + + test('Path rename using absolute file path should anchor to workspace root', async () => { + const uri = workspacePath('sub', 'doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](/sub/doc.md)`, + `[ref]: /sub/doc.md`, + )); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/newSub/newDoc.md', new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + originalUri: uri, + newUri: workspacePath('newSub', 'newDoc.md'), + }, { + uri: uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/newSub/newDoc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/newSub/newDoc.md'), + ] + }); + }); + + test('Path rename should use un-encoded paths as placeholder', async () => { + const uri = workspacePath('sub', 'doc with spaces.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](/sub/doc%20with%20spaces.md)`, + )); + + const info = await prepareRename(doc, new vscode.Position(0, 10), new InMemoryWorkspaceMarkdownDocuments([doc])); + assert.strictEqual(info!.placeholder, '/sub/doc with spaces.md'); + }); + + test('Path rename should encode paths', async () => { + const uri = workspacePath('sub', 'doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](/sub/doc.md)`, + `[ref]: /sub/doc.md`, + )); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/NEW sub/new DOC.md', new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + originalUri: uri, + newUri: workspacePath('NEW sub', 'new DOC.md'), + }, { + uri: uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/NEW%20sub/new%20DOC.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/NEW%20sub/new%20DOC.md'), + ] + }); + }); + + test('Path rename should work with unknown files', async () => { + const uri1 = workspacePath('doc1.md'); + const doc1 = new InMemoryDocument(uri1, joinLines( + `![img](/images/more/image.png)`, + ``, + `[ref]: /images/more/image.png`, + )); + + const uri2 = workspacePath('sub', 'doc2.md'); + const doc2 = new InMemoryDocument(uri2, joinLines( + `![img](/images/more/image.png)`, + )); + + const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), '/img/test/new.png', new InMemoryWorkspaceMarkdownDocuments([ + doc1, + doc2 + ])); + assertEditsEqual(edit!, { + originalUri: workspacePath('images', 'more', 'image.png'), + newUri: workspacePath('img', 'test', 'new.png'), + }, { + uri: uri1, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'), + new vscode.TextEdit(new vscode.Range(2, 7, 2, 29), '/img/test/new.png'), + ] + }, { + uri: uri2, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 29), '/img/test/new.png'), + ] + }); + }); + + test('Path rename should use .md extension on extension-less link', async () => { + const uri = workspacePath('doc.md'); + const doc = new InMemoryDocument(uri, joinLines( + `[text](/doc#header)`, + `[ref]: /doc#other`, + )); + + const edit = await getRenameEdits(doc, new vscode.Position(0, 10), '/new File', new InMemoryWorkspaceMarkdownDocuments([doc])); + assertEditsEqual(edit!, { + originalUri: uri, + newUri: workspacePath('new File.md'), // Rename on disk should use file extension + }, { + uri: uri, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 11), '/new%20File'), // Links should continue to use extension-less paths + new vscode.TextEdit(new vscode.Range(1, 7, 1, 11), '/new%20File'), + ] + }); + }); + + test('Path rename should use correctly resolved paths across files', async () => { + const uri1 = workspacePath('sub', 'doc.md'); + const doc1 = new InMemoryDocument(uri1, joinLines( + `[text](./doc.md)`, + `[ref]: ./doc.md`, + )); + + const uri2 = workspacePath('doc2.md'); + const doc2 = new InMemoryDocument(uri2, joinLines( + `[text](./sub/doc.md)`, + `[ref]: ./sub/doc.md`, + )); + + const uri3 = workspacePath('sub2', 'doc3.md'); + const doc3 = new InMemoryDocument(uri3, joinLines( + `[text](../sub/doc.md)`, + `[ref]: ../sub/doc.md`, + )); + + const uri4 = workspacePath('sub2', 'doc4.md'); + const doc4 = new InMemoryDocument(uri4, joinLines( + `[text](/sub/doc.md)`, + `[ref]: /sub/doc.md`, + )); + + const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), './new/new-doc.md', new InMemoryWorkspaceMarkdownDocuments([ + doc1, doc2, doc3, doc4, + ])); + assertEditsEqual(edit!, { + originalUri: uri1, + newUri: workspacePath('sub', 'new', 'new-doc.md'), + }, { + uri: uri1, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 15), './new/new-doc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 15), './new/new-doc.md'), + ] + }, { + uri: uri2, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 19), './sub/new/new-doc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 19), './sub/new/new-doc.md'), + ] + }, { + uri: uri3, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 20), '../sub/new/new-doc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 20), '../sub/new/new-doc.md'), + ] + }, { + uri: uri4, edits: [ + new vscode.TextEdit(new vscode.Range(0, 7, 0, 18), '/sub/new/new-doc.md'), + new vscode.TextEdit(new vscode.Range(1, 7, 1, 18), '/sub/new/new-doc.md'), + ] + }); + }); + + test('Path rename should resolve on links without prefix', async () => { + const uri1 = workspacePath('sub', 'doc.md'); + const doc1 = new InMemoryDocument(uri1, joinLines( + `![text](images/cat.gif)`, + )); + + const uri2 = workspacePath('doc2.md'); + const doc2 = new InMemoryDocument(uri2, joinLines( + `![text](sub/images/cat.gif)`, + )); + + const edit = await getRenameEdits(doc1, new vscode.Position(0, 10), 'img/cat.gif', new InMemoryWorkspaceMarkdownDocuments([ + doc1, doc2, + ])); + assertEditsEqual(edit!, { + originalUri: workspacePath('sub', 'images', 'cat.gif'), + newUri: workspacePath('sub', 'img', 'cat.gif'), + }, { + uri: uri1, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 22), 'img/cat.gif')] + }, { + uri: uri2, edits: [new vscode.TextEdit(new vscode.Range(0, 8, 0, 26), 'sub/img/cat.gif')] + }); }); test('Rename on link should use header text as placeholder', async () => { @@ -357,7 +582,7 @@ suite('markdown: rename', () => { assertRangeEqual(info!.range, new vscode.Range(1, 8, 1, 13)); }); - test('Rename on http uri should work ', async () => { + test('Rename on http uri should work', async () => { const uri1 = workspacePath('doc.md'); const uri2 = workspacePath('doc2.md'); const doc = new InMemoryDocument(uri1, joinLines( diff --git a/extensions/microsoft-authentication/media/favicon.ico b/extensions/microsoft-authentication/media/favicon.ico new file mode 100644 index 00000000000..7d1a59f7bda Binary files /dev/null and b/extensions/microsoft-authentication/media/favicon.ico differ diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 8321b5682be..6d02e2472b7 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -10,7 +10,6 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { v4 as uuid } from 'uuid'; import fetch, { Response } from 'node-fetch'; -import { Keychain } from './keychain'; import Logger from './logger'; import { toBase64UrlEncoding } from './utils'; import { sha256 } from './env/node/sha256'; @@ -41,18 +40,6 @@ interface IToken { sessionId: string; // The account id + the scope } -interface ITokenClaims { - tid: string; - email?: string; - unique_name?: string; - exp?: number; - preferred_username?: string; - oid?: string; - altsecid?: string; - ipd?: string; - scp: string; -} - interface IStoredSession { id: string; refreshToken: string; @@ -125,10 +112,6 @@ export class AzureActiveDirectoryService { let sessions = await this._tokenStorage.getAll(); Logger.info(`Got ${sessions.length} stored sessions`); - if (!sessions.length) { - sessions = await this.migrate(); - } - const refreshes = sessions.map(async session => { Logger.trace(`Read the following stored session with scopes: ${session.scope}`); const scopes = session.scope.split(' '); @@ -158,7 +141,16 @@ export class AzureActiveDirectoryService { } else { vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); Logger.error(e); - await this.removeSession(session.id); + await this.removeSessionByIToken({ + accessToken: undefined, + refreshToken: session.refreshToken, + account: { + label: session.account.label ?? session.account.displayName!, + id: session.account.id + }, + scope: session.scope, + sessionId: session.id + }); } } }); @@ -391,27 +383,16 @@ export class AzureActiveDirectoryService { }); } - public async removeSession(sessionId: string, writeToDisk: boolean = true): Promise { + public removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise { Logger.info(`Logging out of session '${sessionId}'`); const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId); if (tokenIndex === -1) { Logger.info(`Session not found '${sessionId}'`); - return undefined; + return Promise.resolve(undefined); } - const token = this._tokens[tokenIndex]; - this._tokens.splice(tokenIndex, 1); - this.removeSessionTimeout(sessionId); - - if (writeToDisk) { - await this._tokenStorage.delete(sessionId); - } - - const session = this.convertToSessionSync(token); - Logger.info(`Sending change event for session that was removed with scopes: ${token.scope}`); - onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); - Logger.info(`Logged out of session '${sessionId}' with scopes: ${token.scope}`); - return session; + const token = this._tokens.splice(tokenIndex, 1)[0]; + return this.removeSessionByIToken(token, writeToDisk); } public async clearSessions() { @@ -426,6 +407,25 @@ export class AzureActiveDirectoryService { this._refreshTimeouts.clear(); } + private async removeSessionByIToken(token: IToken, writeToDisk: boolean = true): Promise { + this.removeSessionTimeout(token.sessionId); + + if (writeToDisk) { + await this._tokenStorage.delete(token.sessionId); + } + + const tokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId); + if (tokenIndex !== -1) { + this._tokens.splice(tokenIndex, 1); + } + + const session = this.convertToSessionSync(token); + Logger.info(`Sending change event for session that was removed with scopes: ${token.scope}`); + onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); + Logger.info(`Logged out of session '${token.sessionId}' with scopes: ${token.scope}`); + return session; + } + //#endregion //#region timeout @@ -440,7 +440,7 @@ export class AzureActiveDirectoryService { } catch (e) { if (e.message !== REFRESH_NETWORK_FAILURE) { vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); - await this.removeSession(sessionId); + await this.removeSessionById(sessionId); } } }, timeout)); @@ -797,7 +797,7 @@ export class AzureActiveDirectoryService { // Network failures will automatically retry on next poll. if (e.message !== REFRESH_NETWORK_FAILURE) { vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed.")); - await this.removeSession(session.id); + await this.removeSessionById(session.id); } return; } @@ -806,39 +806,13 @@ export class AzureActiveDirectoryService { for (const { value } of e.removed) { Logger.info(`Session removed in another window with scopes: ${value.scope}`); - const session = await this.removeSession(value.id, false); + const session = await this.removeSessionById(value.id, false); if (session) { removed.push(session); } } } - private async migrate() { - Logger.info('Attempting to migrate stored sessions.'); - const migrated = this._context.globalState.get<{ migrated: boolean }>('microsoft-better-storage-layout-migrated'); - if (migrated?.migrated) { - return []; - } - await this._context.globalState.update('microsoft-better-storage-layout-migrated', { migrated: true }); - const keychain = new Keychain(this._context); - const storedData = await keychain.getToken(); - if (!storedData) { - Logger.info('No stored sessions found.'); - return []; - } - - try { - const sessions = JSON.parse(storedData) as IStoredSession[]; - Logger.info(`Migrated ${sessions.length} stored sessions.`); - return sessions; - } catch (e) { - Logger.info('Failed to parse stored sessions. Migrating no sessions.'); - return []; - } finally { - await keychain.deleteToken(); - } - } - //#endregion //#region static methods diff --git a/extensions/microsoft-authentication/src/authServer.ts b/extensions/microsoft-authentication/src/authServer.ts index c36e56175de..de08c6fca0f 100644 --- a/extensions/microsoft-authentication/src/authServer.ts +++ b/extensions/microsoft-authentication/src/authServer.ts @@ -109,7 +109,8 @@ export class LoopbackAuthServer implements ILoopbackServer { case '/callback': { const code = reqUrl.searchParams.get('code') ?? undefined; const state = reqUrl.searchParams.get('state') ?? undefined; - if (!code || !state) { + const nonce = (reqUrl.searchParams.get('nonce') ?? '').replace(/ /g, '+'); + if (!code || !state || !nonce) { res.writeHead(400); res.end(); return; @@ -119,6 +120,11 @@ export class LoopbackAuthServer implements ILoopbackServer { res.end(); throw new Error('State does not match.'); } + if (this.nonce !== nonce) { + res.writeHead(302, { location: `/?error=${encodeURIComponent('Nonce does not match.')}` }); + res.end(); + throw new Error('Nonce does not match.'); + } deferred.resolve({ code, state }); res.writeHead(302, { location: '/' }); res.end(); diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index f43467f2b91..06a2facdf13 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -48,7 +48,7 @@ export async function activate(context: vscode.ExtensionContext) { */ telemetryReporter.sendTelemetryEvent('logout'); - const session = await loginService.removeSession(id); + const session = await loginService.removeSessionById(id); if (session) { onDidChangeSessions.fire({ added: [], removed: [session], changed: [] }); } diff --git a/extensions/microsoft-authentication/src/keychain.ts b/extensions/microsoft-authentication/src/keychain.ts deleted file mode 100644 index f9704892887..00000000000 --- a/extensions/microsoft-authentication/src/keychain.ts +++ /dev/null @@ -1,60 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import Logger from './logger'; -import * as nls from 'vscode-nls'; - -const localize = nls.loadMessageBundle(); - -const SERVICE_ID = `microsoft.login`; - -export class Keychain { - - constructor(private context: vscode.ExtensionContext) { } - - async setToken(token: string): Promise { - - try { - return await this.context.secrets.store(SERVICE_ID, token); - } catch (e) { - Logger.error(`Setting token failed: ${e}`); - - // Temporary fix for #94005 - // This happens when processes write simulatenously to the keychain, most - // likely when trying to refresh the token. Ignore the error since additional - // writes after the first one do not matter. Should actually be fixed upstream. - if (e.message === 'The specified item already exists in the keychain.') { - return; - } - - const troubleshooting = localize('troubleshooting', "Troubleshooting Guide"); - const result = await vscode.window.showErrorMessage(localize('keychainWriteError', "Writing login information to the keychain failed with error '{0}'.", e.message), troubleshooting); - if (result === troubleshooting) { - vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/docs/editor/settings-sync#_troubleshooting-keychain-issues')); - } - } - } - - async getToken(): Promise { - try { - return await this.context.secrets.get(SERVICE_ID); - } catch (e) { - // Ignore - Logger.error(`Getting token failed: ${e}`); - return Promise.resolve(undefined); - } - } - - async deleteToken(): Promise { - try { - return await this.context.secrets.delete(SERVICE_ID); - } catch (e) { - // Ignore - Logger.error(`Deleting token failed: ${e}`); - return Promise.resolve(undefined); - } - } -} diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 8ad65a8be63..1d6d75aa4f7 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -174,7 +174,7 @@ export const activate: ActivationFunction = (ctx) => { .output-plaintext, .output-stream, .traceback { - line-height: 22px; + line-height: var(--notebook-cell-output-line-height); font-family: var(--notebook-cell-output-font-family); white-space: pre-wrap; word-wrap: break-word; @@ -185,6 +185,9 @@ export const activate: ActivationFunction = (ctx) => { -ms-user-select: text; cursor: auto; } + span.output-stream { + display: inline-block; + } .output-plaintext .code-bold, .output-stream .code-bold, .traceback .code-bold { diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index f4da67b93f2..d05b7bbc8bf 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -67,7 +67,7 @@ export class NpmTaskProvider implements TaskProvider { return undefined; } if (kind.path) { - packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/' + kind.path + 'package.json' }); + packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/' + kind.path + `${kind.path.endsWith('/') ? '' : '/'}` + 'package.json' }); } else { packageJsonUri = _task.scope.uri.with({ path: _task.scope.uri.path + '/package.json' }); } @@ -324,7 +324,7 @@ export async function createTask(packageManager: string, script: NpmTaskDefiniti } let relativePackageJson = getRelativePath(packageJsonUri); - if (relativePackageJson.length) { + if (relativePackageJson.length && !kind.path) { kind.path = relativePackageJson.substring(0, relativePackageJson.length - 1); } let taskName = getTaskName(kind.script, relativePackageJson); diff --git a/extensions/shellscript/cgmanifest.json b/extensions/shellscript/cgmanifest.json index fcf7f676fa7..6f0ae3a254e 100644 --- a/extensions/shellscript/cgmanifest.json +++ b/extensions/shellscript/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "atom/language-shellscript", "repositoryUrl": "https://github.com/atom/language-shellscript", - "commitHash": "4c3711edbe8eac6f501976893976b1ac6a043d50" + "commitHash": "4f8d7bb5cc4d1643674551683df10fe552dd5a6f" } }, "license": "MIT", "description": "The file syntaxes/shell-unix-bash.tmLanguage.json was derived from the Atom package https://github.com/atom/language-shellscript which was originally converted from the TextMate bundle https://github.com/textmate/shellscript.tmbundle.", - "version": "0.26.0" + "version": "0.28.2" } ], "version": 1 diff --git a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json index 0089e5aa5c4..bfbb5f135d0 100644 --- a/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json +++ b/extensions/shellscript/syntaxes/shell-unix-bash.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-shellscript/commit/4c3711edbe8eac6f501976893976b1ac6a043d50", + "version": "https://github.com/atom/language-shellscript/commit/4f8d7bb5cc4d1643674551683df10fe552dd5a6f", "name": "Shell Script", "scopeName": "source.shell", "patterns": [ @@ -624,7 +624,7 @@ ] }, { - "begin": "(<<)-\\s*(\"|'|)\\s*\\\\?([^;&<\\s]+)\\2", + "begin": "(<<)-\\s*(\"|')\\s*\\\\?([^;&<\\s]+)\\2", "beginCaptures": { "1": { "name": "keyword.operator.heredoc.shell" @@ -642,7 +642,7 @@ "name": "string.unquoted.heredoc.no-indent.shell" }, { - "begin": "(<<)\\s*(\"|'|)\\s*\\\\?([^;&<\\s]+)\\2", + "begin": "(<<)\\s*(\"|')\\s*\\\\?([^;&<\\s]+)\\2", "beginCaptures": { "1": { "name": "keyword.operator.heredoc.shell" @@ -658,6 +658,66 @@ } }, "name": "string.unquoted.heredoc.shell" + }, + { + "begin": "(<<)-\\s*\\\\?([^;&<\\s]+)", + "beginCaptures": { + "1": { + "name": "keyword.operator.heredoc.shell" + }, + "2": { + "name": "keyword.control.heredoc-token.shell" + } + }, + "end": "^\\t*(\\2)(?=\\s|;|&|$)", + "endCaptures": { + "1": { + "name": "keyword.control.heredoc-token.shell" + } + }, + "name": "string.unquoted.heredoc.expanded.no-indent.shell", + "patterns": [ + { + "match": "\\\\[\\$`\\\\\\n]", + "name": "constant.character.escape.shell" + }, + { + "include": "#variable" + }, + { + "include": "#interpolation" + } + ] + }, + { + "begin": "(<<)\\s*\\\\?([^;&<\\s]+)", + "beginCaptures": { + "1": { + "name": "keyword.operator.heredoc.shell" + }, + "2": { + "name": "keyword.control.heredoc-token.shell" + } + }, + "end": "^(\\2)(?=\\s|;|&|$)", + "endCaptures": { + "1": { + "name": "keyword.control.heredoc-token.shell" + } + }, + "name": "string.unquoted.heredoc.expanded.shell", + "patterns": [ + { + "match": "\\\\[\\$`\\\\\\n]", + "name": "constant.character.escape.shell" + }, + { + "include": "#variable" + }, + { + "include": "#interpolation" + } + ] } ] }, diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index db88efe88ca..932626b6a12 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -65,6 +65,7 @@ "onCommand:_typescript.configurePlugin", "onCommand:_typescript.learnMoreAboutRefactorings", "onCommand:typescript.fileReferences", + "onCommand:typescript.goToSourceDefinition", "onTaskType:typescript", "onLanguage:jsonc" ], @@ -1229,6 +1230,11 @@ "command": "typescript.findAllFileReferences", "title": "%typescript.findAllFileReferences%", "category": "TypeScript" + }, + { + "command": "typescript.goToSourceDefinition", + "title": "%typescript.goToSourceDefinition%", + "category": "TypeScript" } ], "menus": { @@ -1280,6 +1286,32 @@ { "command": "typescript.findAllFileReferences", "when": "tsSupportsFileReferences && typescript.isManagedFile" + }, + { + "command": "typescript.goToSourceDefinition", + "when": "tsSupportsSourceDefinition && typescript.isManagedFile" + } + ], + "editor/context": [ + { + "command": "typescript.goToSourceDefinition", + "when": "tsSupportsSourceDefinition && resourceLangId == typescript", + "group": "navigation@9" + }, + { + "command": "typescript.goToSourceDefinition", + "when": "tsSupportsSourceDefinition && resourceLangId == typescriptreact", + "group": "navigation@9" + }, + { + "command": "typescript.goToSourceDefinition", + "when": "tsSupportsSourceDefinition && resourceLangId == javascript", + "group": "navigation@9" + }, + { + "command": "typescript.goToSourceDefinition", + "when": "tsSupportsSourceDefinition && resourceLangId == javascriptreact", + "group": "navigation@9" } ], "explorer/context": [ diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 4a7322a8432..5a91821e729 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -190,6 +190,7 @@ "codeActions.refactor.rewrite.property.generateAccessors.description": "Generate 'get' and 'set' accessors", "codeActions.source.organizeImports.title": "Organize imports", "typescript.findAllFileReferences": "Find File References", + "typescript.goToSourceDefinition": "Go to Source Definition", "configuration.suggest.classMemberSnippets.enabled": "Enable/disable snippet completions for class members. Requires using TypeScript 4.5+ in the workspace", "configuration.suggest.objectLiteralMethodSnippets.enabled": "Enable/disable snippet completions for methods in object literals. Requires using TypeScript 4.7+ in the workspace" } diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts index 97287b9678d..890486d7902 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts @@ -8,6 +8,8 @@ import * as nls from 'vscode-nls'; import type * as Proto from '../../protocol'; import { CachedResponse } from '../../tsServer/cachedResponse'; import { ITypeScriptServiceClient } from '../../typescriptService'; +import { escapeRegExp } from '../../utils/regexp'; +import * as typeConverters from '../../utils/typeConverters'; const localize = nls.loadMessageBundle(); @@ -57,6 +59,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensP } protected abstract extractSymbol( + document: vscode.TextDocument, item: Proto.NavigationTree, parent: Proto.NavigationTree | undefined ): vscode.Range | undefined; @@ -67,7 +70,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensP parent: Proto.NavigationTree | undefined, results: vscode.Range[] ): void { - const range = this.extractSymbol(item, parent); + const range = this.extractSymbol(document, item, parent); if (range) { results.push(range); } @@ -75,3 +78,29 @@ export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensP item.childItems?.forEach(child => this.walkNavTree(document, child, item, results)); } } + +export function getSymbolRange( + document: vscode.TextDocument, + item: Proto.NavigationTree +): vscode.Range | undefined { + if (item.nameSpan) { + return typeConverters.Range.fromTextSpan(item.nameSpan); + } + + // In older versions, we have to calculate this manually. See #23924 + const span = item.spans && item.spans[0]; + if (!span) { + return undefined; + } + + const range = typeConverters.Range.fromTextSpan(span); + const text = document.getText(range); + + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`, 'gm'); + const match = identifierMatch.exec(text); + const prefixLength = match ? match.index + match[1].length : 0; + const startOffset = document.offsetAt(new vscode.Position(range.start.line, range.start.character)) + prefixLength; + return new vscode.Range( + document.positionAt(startOffset), + document.positionAt(startOffset + item.text.length)); +} diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts index 4583d6d25ec..37750021597 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/implementationsCodeLens.ts @@ -13,7 +13,7 @@ import { conditionalRegistration, requireGlobalConfiguration, requireSomeCapabil import { DocumentSelector } from '../../utils/documentSelector'; import { LanguageDescription } from '../../utils/languageDescription'; import * as typeConverters from '../../utils/typeConverters'; -import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; +import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -66,26 +66,21 @@ export default class TypeScriptImplementationsCodeLensProvider extends TypeScrip } protected extractSymbol( + document: vscode.TextDocument, item: Proto.NavigationTree, _parent: Proto.NavigationTree | undefined ): vscode.Range | undefined { - if (!item.nameSpan) { - return undefined; - } - - const itemSpan = typeConverters.Range.fromTextSpan(item.nameSpan); - switch (item.kind) { case PConst.Kind.interface: - return itemSpan; + return getSymbolRange(document, item); case PConst.Kind.class: case PConst.Kind.method: case PConst.Kind.memberVariable: case PConst.Kind.memberGetAccessor: case PConst.Kind.memberSetAccessor: - if (/\babstract\b/g.test(item.kindModifiers)) { - return itemSpan; + if (item.kindModifiers.match(/\babstract\b/g)) { + return getSymbolRange(document, item); } break; } diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts index 6f1cf9ca1ce..8e4f0819b7b 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/referencesCodeLens.ts @@ -14,7 +14,7 @@ import { conditionalRegistration, requireGlobalConfiguration, requireSomeCapabil import { DocumentSelector } from '../../utils/documentSelector'; import { LanguageDescription } from '../../utils/languageDescription'; import * as typeConverters from '../../utils/typeConverters'; -import { ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; +import { getSymbolRange, ReferencesCodeLens, TypeScriptBaseCodeLensProvider } from './baseCodeLensProvider'; const localize = nls.loadMessageBundle(); @@ -61,24 +61,19 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens } protected extractSymbol( + document: vscode.TextDocument, item: Proto.NavigationTree, parent: Proto.NavigationTree | undefined ): vscode.Range | undefined { - if (!item.nameSpan) { - return undefined; - } - - const itemSpan = typeConverters.Range.fromTextSpan(item.nameSpan); - if (parent && parent.kind === PConst.Kind.enum) { - return itemSpan; + return getSymbolRange(document, item); } switch (item.kind) { case PConst.Kind.function: { const showOnAllFunctions = vscode.workspace.getConfiguration(this.language.id).get('referencesCodeLens.showOnAllFunctions'); if (showOnAllFunctions) { - return itemSpan; + return getSymbolRange(document, item); } } // fallthrough @@ -88,7 +83,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.variable: // Only show references for exported variables if (/\bexport\b/.test(item.kindModifiers)) { - return itemSpan; + return getSymbolRange(document, item); } break; @@ -96,12 +91,12 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens if (item.text === '') { break; } - return itemSpan; + return getSymbolRange(document, item); case PConst.Kind.interface: case PConst.Kind.type: case PConst.Kind.enum: - return itemSpan; + return getSymbolRange(document, item); case PConst.Kind.method: case PConst.Kind.memberGetAccessor: @@ -121,7 +116,7 @@ export class TypeScriptReferencesCodeLensProvider extends TypeScriptBaseCodeLens case PConst.Kind.class: case PConst.Kind.interface: case PConst.Kind.type: - return itemSpan; + return getSymbolRange(document, item); } break; } diff --git a/extensions/typescript-language-features/src/languageFeatures/sourceDefinition.ts b/extensions/typescript-language-features/src/languageFeatures/sourceDefinition.ts new file mode 100644 index 00000000000..d020d265995 --- /dev/null +++ b/extensions/typescript-language-features/src/languageFeatures/sourceDefinition.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import { Command, CommandManager } from '../commands/commandManager'; +import * as Proto from '../protocol'; +import { ExecConfig, ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; +import API from '../utils/api'; +import { isSupportedLanguageMode } from '../utils/languageIds'; +import * as typeConverters from '../utils/typeConverters'; + +const localize = nls.loadMessageBundle(); + +namespace ExperimentalProto { + export const enum CommandTypes { + FindSourceDefinition = 'findSourceDefinition' + } + + export interface SourceDefinitionRequestArgs extends Proto.FileLocationRequestArgs { } + + export interface SourceDefinitionRequest extends Proto.Request { + command: CommandTypes.FindSourceDefinition; + arguments: SourceDefinitionRequestArgs; + } + + export interface InlayHintsResponse extends Proto.DefinitionResponse { } + + export interface IExtendedTypeScriptServiceClient { + execute( + command: K, + args: ExtendedTsServerRequests[K][0], + token: vscode.CancellationToken, + config?: ExecConfig + ): Promise>; + } + + export interface ExtendedTsServerRequests { + 'findSourceDefinition': [SourceDefinitionRequestArgs, InlayHintsResponse]; + } +} + +class SourceDefinitionCommand implements Command { + + public static readonly context = 'tsSupportsSourceDefinition'; + public static readonly minVersion = API.v470; + + public readonly id = 'typescript.goToSourceDefinition'; + + public constructor( + private readonly client: ITypeScriptServiceClient + ) { } + + public async execute() { + if (this.client.apiVersion.lt(SourceDefinitionCommand.minVersion)) { + vscode.window.showErrorMessage(localize('error.unsupportedVersion', "Go to Source Definition failed. Requires TypeScript 4.7+.")); + return; + } + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + vscode.window.showErrorMessage(localize('error.noResource', "Go to Source Definition failed. No resource provided.")); + return; + } + + const resource = activeEditor.document.uri; + const document = await vscode.workspace.openTextDocument(resource); + if (!isSupportedLanguageMode(document)) { + vscode.window.showErrorMessage(localize('error.unsupportedLanguage', "Go to Source Definition failed. Unsupported file type.")); + return; + } + + const openedFiledPath = this.client.toOpenedFilePath(document); + if (!openedFiledPath) { + vscode.window.showErrorMessage(localize('error.unknownFile', "Go to Source Definition failed. Unknown file type.")); + return; + } + + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Window, + title: localize('progress.title', "Finding source definitions") + }, async (_progress, token) => { + + const position = activeEditor.selection.anchor; + const args = typeConverters.Position.toFileLocationRequestArgs(openedFiledPath, position); + const response = await (this.client as ExperimentalProto.IExtendedTypeScriptServiceClient).execute('findSourceDefinition', args, token); + if (response.type === 'response' && response.body) { + const locations: vscode.Location[] = response.body.map(reference => + typeConverters.Location.fromTextSpan(this.client.toResource(reference.file), reference)); + + if (locations.length) { + if (locations.length === 1) { + vscode.commands.executeCommand('vscode.open', locations[0].uri.with({ + fragment: `L${locations[0].range.start.line + 1},${locations[0].range.start.character + 1}` + })); + } else { + vscode.commands.executeCommand('editor.action.showReferences', resource, position, locations); + } + return; + } + } + + vscode.window.showErrorMessage(localize('error.noReferences', "No source definitions found.")); + }); + } +} + + +export function register( + client: ITypeScriptServiceClient, + commandManager: CommandManager +) { + function updateContext() { + vscode.commands.executeCommand('setContext', SourceDefinitionCommand.context, client.apiVersion.gte(SourceDefinitionCommand.minVersion)); + } + updateContext(); + + commandManager.register(new SourceDefinitionCommand(client)); + return client.onTsServerStarted(() => updateContext()); +} diff --git a/extensions/typescript-language-features/src/languageProvider.ts b/extensions/typescript-language-features/src/languageProvider.ts index 4a3669e142d..321a2c845b3 100644 --- a/extensions/typescript-language-features/src/languageProvider.ts +++ b/extensions/typescript-language-features/src/languageProvider.ts @@ -73,6 +73,7 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/formatting').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), import('./languageFeatures/hover').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager))), import('./languageFeatures/implementations').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/inlayHints').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), import('./languageFeatures/jsDocCompletions').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), import('./languageFeatures/organizeImports').then(provider => this._register(provider.register(selector, this.client, this.commandManager, this.fileConfigurationManager, this.telemetryReporter))), import('./languageFeatures/quickFix').then(provider => this._register(provider.register(selector, this.client, this.fileConfigurationManager, this.commandManager, this.client.diagnosticsManager, this.telemetryReporter))), @@ -82,9 +83,9 @@ export default class LanguageProvider extends Disposable { import('./languageFeatures/semanticTokens').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/signatureHelp').then(provider => this._register(provider.register(selector, this.client))), import('./languageFeatures/smartSelect').then(provider => this._register(provider.register(selector, this.client))), + import('./languageFeatures/sourceDefinition').then(provider => this._register(provider.register(this.client, this.commandManager))), import('./languageFeatures/tagClosing').then(provider => this._register(provider.register(selector, this.description, this.client))), import('./languageFeatures/typeDefinitions').then(provider => this._register(provider.register(selector, this.client))), - import('./languageFeatures/inlayHints').then(provider => this._register(provider.register(selector, this.description, this.client, this.fileConfigurationManager))), ]); } diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index e4c6133cc15..e9e85091fa4 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -39,6 +39,7 @@ export default class API { public static readonly v430 = API.fromSimpleString('4.3.0'); public static readonly v440 = API.fromSimpleString('4.4.0'); public static readonly v460 = API.fromSimpleString('4.6.0'); + public static readonly v470 = API.fromSimpleString('4.7.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 22a274a9c68..501c623ba93 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -13,18 +13,17 @@ "documentFiltersExclusive", "editorInsets", "extensionRuntime", + "extensionsAny", "externalUriOpener", "fileSearchProvider", "findTextInFiles", "fsChunks", "inlineCompletions", "notebookCellExecutionState", - "notebookConcatTextDocument", "notebookContentProvider", "notebookControllerKind", "notebookDebugOptions", "notebookDeprecated", - "notebookDocumentEvents", "notebookEditor", "notebookEditorDecorationType", "notebookEditorEdit", @@ -37,7 +36,6 @@ "scmActionButton", "scmSelectedProvider", "scmValidation", - "tabs", "taskPresentationGroup", "terminalDataWriteEvent", "terminalDimensions", diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts index c120526ae14..462e3113698 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.test.ts @@ -1001,7 +1001,9 @@ suite('Notebook & LiveShare', function () { suiteDisposables.push(vscode.workspace.registerNotebookSerializer(notebookType, new class implements vscode.NotebookSerializer { deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData | Thenable { const value = new TextDecoder().decode(content); - return new vscode.NotebookData([new vscode.NotebookCellData(vscode.NotebookCellKind.Code, value, 'fooLang')]); + const cell1 = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, value, 'fooLang'); + cell1.outputs = [new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(value)])]; + return new vscode.NotebookData([cell1]); } serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array | Thenable { return new TextEncoder().encode(data.cells[0].value); @@ -1030,6 +1032,7 @@ suite('Notebook & LiveShare', function () { assert.ok(data instanceof vscode.NotebookData); assert.strictEqual(data.cells.length, 1); assert.strictEqual(data.cells[0].value, value); + assert.strictEqual(new TextDecoder().decode(data.cells[0].outputs![0].items[0].data), value); }); test('command: vscode.executeNotebookToData', async function () { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 4af3c975e70..33a5fe95eb2 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -26,6 +26,7 @@ import { assertNoRpc, poll } from '../utils'; await config.update('gpuAcceleration', 'off', ConfigurationTarget.Global); // Disable env var relaunch for tests to prevent terminals relaunching themselves await config.update('environmentChangesRelaunch', false, ConfigurationTarget.Global); + await config.update('shellIntegration.enabled', false); }); suite('Terminal', () => { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 463c78c40bb..5d004bb6db7 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { join } from 'path'; -import { CancellationTokenSource, commands, MarkdownString, TabKindNotebook, Position, QuickPickItem, Selection, StatusBarAlignment, TabKindTextDiff, TextEditor, TextEditorSelectionChangeKind, TextEditorViewColumnChangeEvent, TabKindText, Uri, ViewColumn, window, workspace } from 'vscode'; +import { CancellationTokenSource, commands, MarkdownString, TabInputNotebook, Position, QuickPickItem, Selection, StatusBarAlignment, TextEditor, TextEditorSelectionChangeKind, TextEditorViewColumnChangeEvent, TabInputText, Uri, ViewColumn, window, workspace, TabInputTextDiff } from 'vscode'; import { assertNoRpc, closeAllEditors, createRandomFile, pathEquals } from '../utils'; @@ -371,28 +371,28 @@ suite('vscode API - window', () => { }); //#region Tabs API tests - test('Tabs - move tab', async function () { - const [docA, docB, docC] = await Promise.all([ - workspace.openTextDocument(await createRandomFile()), - workspace.openTextDocument(await createRandomFile()), - workspace.openTextDocument(await createRandomFile()) - ]); + // test('Tabs - move tab', async function () { + // const [docA, docB, docC] = await Promise.all([ + // workspace.openTextDocument(await createRandomFile()), + // workspace.openTextDocument(await createRandomFile()), + // workspace.openTextDocument(await createRandomFile()) + // ]); - await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false }); - await window.showTextDocument(docB, { viewColumn: ViewColumn.One, preview: false }); - await window.showTextDocument(docC, { viewColumn: ViewColumn.Two, preview: false }); + // await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false }); + // await window.showTextDocument(docB, { viewColumn: ViewColumn.One, preview: false }); + // await window.showTextDocument(docC, { viewColumn: ViewColumn.Two, preview: false }); - const tabGroups = window.tabGroups; - assert.strictEqual(tabGroups.all.length, 2); + // const tabGroups = window.tabGroups; + // assert.strictEqual(tabGroups.all.length, 2); - const group1Tabs = tabGroups.all[0].tabs; - assert.strictEqual(group1Tabs.length, 2); + // const group1Tabs = tabGroups.all[0].tabs; + // assert.strictEqual(group1Tabs.length, 2); - const group2Tabs = tabGroups.all[1].tabs; - assert.strictEqual(group2Tabs.length, 1); + // const group2Tabs = tabGroups.all[1].tabs; + // assert.strictEqual(group2Tabs.length, 1); - await tabGroups.move(group1Tabs[0], ViewColumn.One, 1); - }); + // await tabGroups.move(group1Tabs[0], ViewColumn.One, 1); + // }); // TODO @lramos15 re-enable these once shape is more stable test('Tabs - vscode.open & vscode.diff', async function () { @@ -423,14 +423,14 @@ suite('vscode API - window', () => { const tabs = window.tabGroups.all.map(g => g.tabs).flat(1); assert.strictEqual(tabs.length, 5); - assert.ok(tabs[0].kind instanceof TabKindText); - assert.strictEqual(tabs[0].kind.uri.toString(), docA.uri.toString()); - assert.ok(tabs[1].kind instanceof TabKindText); - assert.strictEqual(tabs[1].kind.uri.toString(), docB.uri.toString()); - assert.ok(tabs[2].kind instanceof TabKindText); - assert.strictEqual(tabs[2].kind.uri.toString(), docC.uri.toString()); - assert.ok(tabs[3].kind instanceof TabKindText); - assert.strictEqual(tabs[3].kind.uri.toString(), commandFile.toString()); + assert.ok(tabs[0].input instanceof TabInputText); + assert.strictEqual(tabs[0].input.uri.toString(), docA.uri.toString()); + assert.ok(tabs[1].input instanceof TabInputText); + assert.strictEqual(tabs[1].input.uri.toString(), docB.uri.toString()); + assert.ok(tabs[2].input instanceof TabInputText); + assert.strictEqual(tabs[2].input.uri.toString(), docC.uri.toString()); + assert.ok(tabs[3].input instanceof TabInputText); + assert.strictEqual(tabs[3].input.uri.toString(), commandFile.toString()); }); test('Tabs - Ensure tabs getter is correct', async function () { @@ -459,17 +459,17 @@ suite('vscode API - window', () => { assert.strictEqual(tabs.length, 5); // All resources should match the text documents as they're the only tabs currently open - assert.ok(tabs[0].kind instanceof TabKindText); - assert.strictEqual(tabs[0].kind.uri.toString(), docA.uri.toString()); - assert.ok(tabs[1].kind instanceof TabKindNotebook); - assert.strictEqual(tabs[1].kind.uri.toString(), notebookDoc.uri.toString()); - assert.ok(tabs[2].kind instanceof TabKindText); - assert.strictEqual(tabs[2].kind.uri.toString(), docB.uri.toString()); - assert.ok(tabs[3].kind instanceof TabKindText); - assert.strictEqual(tabs[3].kind.uri.toString(), docC.uri.toString()); + assert.ok(tabs[0].input instanceof TabInputText); + assert.strictEqual(tabs[0].input.uri.toString(), docA.uri.toString()); + assert.ok(tabs[1].input instanceof TabInputNotebook); + assert.strictEqual(tabs[1].input.uri.toString(), notebookDoc.uri.toString()); + assert.ok(tabs[2].input instanceof TabInputText); + assert.strictEqual(tabs[2].input.uri.toString(), docB.uri.toString()); + assert.ok(tabs[3].input instanceof TabInputText); + assert.strictEqual(tabs[3].input.uri.toString(), docC.uri.toString()); // Diff editor and side by side editor report the right side as the resource - assert.ok(tabs[4].kind instanceof TabKindTextDiff); - assert.strictEqual(tabs[4].kind.modified.toString(), rightDiff.toString()); + assert.ok(tabs[4].input instanceof TabInputTextDiff); + assert.strictEqual(tabs[4].input.modified.toString(), rightDiff.toString()); assert.strictEqual(tabs[0].group.viewColumn, ViewColumn.One); assert.strictEqual(tabs[1].group.viewColumn, ViewColumn.One); @@ -495,20 +495,20 @@ suite('vscode API - window', () => { await window.showTextDocument(docA, { viewColumn: ViewColumn.One, preview: false }); let activeTab = getActiveTabInActiveGroup(); assert.ok(activeTab); - assert.ok(activeTab.kind instanceof TabKindText); - assert.strictEqual(activeTab.kind.uri.toString(), docA.uri.toString()); + assert.ok(activeTab.input instanceof TabInputText); + assert.strictEqual(activeTab.input.uri.toString(), docA.uri.toString()); await window.showTextDocument(docB, { viewColumn: ViewColumn.Two, preview: false }); activeTab = getActiveTabInActiveGroup(); assert.ok(activeTab); - assert.ok(activeTab.kind instanceof TabKindText); - assert.strictEqual(activeTab.kind.uri.toString(), docB.uri.toString()); + assert.ok(activeTab.input instanceof TabInputText); + assert.strictEqual(activeTab.input.uri.toString(), docB.uri.toString()); await window.showTextDocument(docC, { viewColumn: ViewColumn.Three, preview: false }); activeTab = getActiveTabInActiveGroup(); assert.ok(activeTab); - assert.ok(activeTab.kind instanceof TabKindText); - assert.strictEqual(activeTab.kind.uri.toString(), docC.uri.toString()); + assert.ok(activeTab.input instanceof TabInputText); + assert.strictEqual(activeTab.input.uri.toString(), docC.uri.toString()); await commands.executeCommand('workbench.action.closeActiveEditor'); await commands.executeCommand('workbench.action.closeActiveEditor'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index a0c072998aa..4337e8043c8 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1008,6 +1008,30 @@ suite('vscode API - workspace', () => { assert.strictEqual(e.files[1].oldUri.toString(), file2.toString()); }); + test('WorkspaceEdit fails when creating then writing to file if file is open in the editor and is not empty #146964', async function () { + const file1 = await createRandomFile(); + + { + // prepare: open file in editor, make sure it has contents + const editor = await vscode.window.showTextDocument(file1); + const prepEdit = new vscode.WorkspaceEdit(); + prepEdit.insert(file1, new vscode.Position(0, 0), 'Hello Here And There'); + const status = await vscode.workspace.applyEdit(prepEdit); + + assert.ok(status); + assert.strictEqual(editor.document.getText(), 'Hello Here And There'); + assert.ok(vscode.window.activeTextEditor === editor); + } + + const we = new vscode.WorkspaceEdit(); + we.createFile(file1, { overwrite: true, ignoreIfExists: false }); + we.set(file1, [new vscode.TextEdit(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), 'SOME TEXT')]); + const status = await vscode.workspace.applyEdit(we); + assert.ok(status); + assert.strictEqual(vscode.window.activeTextEditor!.document.getText(), 'SOME TEXT'); + + }); + test('Should send a single FileWillRenameEvent instead of separate events when moving multiple files at once#111867, 2/3', async function () { const event = new Promise(resolve => { diff --git a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.sh b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.sh index 4c5bf8f7eab..50751c1e4a7 100644 --- a/extensions/vscode-colorize-tests/test/colorize-fixtures/test.sh +++ b/extensions/vscode-colorize-tests/test/colorize-fixtures/test.sh @@ -10,6 +10,11 @@ fi DEVELOPER=$(xcode-select -print-path) LIPO=$(xcrun -sdk iphoneos -find lipo) +cat <<-EOF > /path/file + # A heredoc with a variable $DEVELOPER + some more file +EOF + function code() { cd $ROOT diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json index a755f73ee61..9fd6ba74f7e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json @@ -1223,6 +1223,126 @@ "hc_light": "string: #A31515" } }, + { + "c": "cat ", + "t": "source.shell", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF", + "hc_light": "default: #292929" + } + }, + { + "c": "<<", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell keyword.operator.heredoc.shell", + "r": { + "dark_plus": "keyword.operator: #D4D4D4", + "light_plus": "keyword.operator: #000000", + "dark_vs": "keyword.operator: #D4D4D4", + "light_vs": "keyword.operator: #000000", + "hc_black": "keyword.operator: #D4D4D4", + "hc_light": "keyword.operator: #000000" + } + }, + { + "c": "-", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "hc_light": "string: #A31515" + } + }, + { + "c": "EOF", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell keyword.control.heredoc-token.shell", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, + { + "c": " > /path/file", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "hc_light": "string: #A31515" + } + }, + { + "c": "\t# A heredoc with a variable ", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "hc_light": "string: #A31515" + } + }, + { + "c": "$", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell variable.other.normal.shell punctuation.definition.variable.shell", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "variable: #9CDCFE", + "hc_light": "variable: #001080" + } + }, + { + "c": "DEVELOPER", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell variable.other.normal.shell", + "r": { + "dark_plus": "variable: #9CDCFE", + "light_plus": "variable: #001080", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "variable: #9CDCFE", + "hc_light": "variable: #001080" + } + }, + { + "c": "\tsome more file", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "hc_light": "string: #A31515" + } + }, + { + "c": "EOF", + "t": "source.shell string.unquoted.heredoc.expanded.no-indent.shell keyword.control.heredoc-token.shell", + "r": { + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0", + "hc_light": "keyword.control: #AF00DB" + } + }, { "c": "function", "t": "source.shell meta.function.shell storage.type.function.shell", diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index e0a672ca179..8792cd4e6fb 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -11,7 +11,6 @@ "main": "./out/extension", "enabledApiProposals": [ "notebookCellExecutionState", - "notebookConcatTextDocument", "notebookContentProvider", "notebookControllerKind", "notebookDebugOptions", diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index b74d79c0cc6..9f6fc9efc18 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -24,7 +24,13 @@ let outputChannel: vscode.OutputChannel; export function activate(context: vscode.ExtensionContext) { + let connectionPaused = false; + let connectionPausedEvent = new vscode.EventEmitter(); + function doResolve(_authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + if (connectionPaused) { + throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now'); + } const connectionToken = String(crypto.randomInt(0xffffffffff)); // eslint-disable-next-line no-async-promise-executor @@ -151,8 +157,8 @@ export function activate(context: vscode.ExtensionContext) { let remoteReady = true, localReady = true; const remoteSocket = net.createConnection({ port: serverAddr.port }); - let isDisconnected = connectionPaused; - connectionPausedEvent.event(_ => { + let isDisconnected = false; + const handleConnectionPause = () => { let newIsDisconnected = connectionPaused; if (isDisconnected !== newIsDisconnected) { outputChannel.appendLine(`Connection state: ${newIsDisconnected ? 'open' : 'paused'}`); @@ -175,7 +181,10 @@ export function activate(context: vscode.ExtensionContext) { } } } - }); + }; + + connectionPausedEvent.event(_ => handleConnectionPause()); + handleConnectionPause(); proxySocket.on('data', (data) => { remoteReady = remoteSocket.write(data); @@ -251,9 +260,6 @@ export function activate(context: vscode.ExtensionContext) { }); } - let connectionPaused = false; - let connectionPausedEvent = new vscode.EventEmitter(); - const authorityResolverDisposable = vscode.workspace.registerRemoteAuthorityResolver('test', { async getCanonicalURI(uri: vscode.Uri): Promise { return vscode.Uri.file(uri.path); diff --git a/package.json b/package.json index 3eaa24c2254..5c02dca7911 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.67.0", - "distro": "3b8074f0f1fad5340a89fadbcfe9d2b3f6869baf", + "distro": "f6b755dd10abe14873ceaec1acbf1e482a322722", "author": { "name": "Microsoft Corporation" }, @@ -84,18 +84,18 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.20", - "xterm-addon-search": "0.9.0-beta.18", + "xterm": "4.19.0-beta.25", + "xterm-addon-search": "0.9.0-beta.22", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.27", - "xterm-headless": "4.19.0-beta.20", + "xterm-addon-webgl": "0.12.0-beta.29", + "xterm-headless": "4.19.0-beta.25", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, "devDependencies": { "7zip": "0.0.6", - "@playwright/test": "1.20.2", + "@playwright/test": "1.21.0", "@types/applicationinsights": "0.20.0", "@types/cookie": "^0.3.3", "@types/copy-webpack-plugin": "^6.0.3", @@ -135,7 +135,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "17.4.0", + "electron": "17.4.1", "eslint": "8.7.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^19.1.0", @@ -161,7 +161,6 @@ "gulp-remote-retry-src": "^0.8.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", - "gulp-shell": "^0.6.5", "gulp-sourcemaps": "^3.0.0", "gulp-svgmin": "^4.1.0", "gulp-tsb": "4.0.6", @@ -201,7 +200,7 @@ "style-loader": "^1.0.0", "ts-loader": "^9.2.7", "tsec": "0.1.4", - "typescript": "4.7.0-dev.20220323", + "typescript": "^4.7.0-dev.20220419", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", diff --git a/remote/package.json b/remote/package.json index 2173d072545..8be2005cde1 100644 --- a/remote/package.json +++ b/remote/package.json @@ -24,12 +24,12 @@ "vscode-proxy-agent": "^0.12.0", "vscode-regexpp": "^3.1.0", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.20", - "xterm-addon-search": "0.9.0-beta.18", + "xterm": "4.19.0-beta.25", + "xterm-addon-search": "0.9.0-beta.22", "xterm-addon-serialize": "0.7.0-beta.12", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.27", - "xterm-headless": "4.19.0-beta.20", + "xterm-addon-webgl": "0.12.0-beta.29", + "xterm-headless": "4.19.0-beta.25", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 63ca1649694..d3dad3b383a 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -10,9 +10,9 @@ "tas-client-umd": "0.1.4", "vscode-oniguruma": "1.6.1", "vscode-textmate": "7.0.1", - "xterm": "4.19.0-beta.20", - "xterm-addon-search": "0.9.0-beta.18", + "xterm": "4.19.0-beta.25", + "xterm-addon-search": "0.9.0-beta.22", "xterm-addon-unicode11": "0.4.0-beta.3", - "xterm-addon-webgl": "0.12.0-beta.27" + "xterm-addon-webgl": "0.12.0-beta.29" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 2579eb0b787..e15a4cf241a 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -113,22 +113,22 @@ vscode-textmate@7.0.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== -xterm-addon-search@0.9.0-beta.18: - version "0.9.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.18.tgz#5317aed1dc747f468ccb7ecd151fb00d82a8a19d" - integrity sha512-SAeA3thc2WJNYXwjOEJFLpZ1ZVOs22RLmz9a6WcrzXkvCjLZRvbRGwX25Ms+Dd7dVDQNbKVUzUJohspP/vYr0Q== +xterm-addon-search@0.9.0-beta.22: + version "0.9.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.22.tgz#18f2eabda82709cdd64dee003879b586869e28a3" + integrity sha512-1I8W0bwjg6bUoNaESKHSkS9BN3Z9Xe44/zbUb6Un+pbb5Nun4LQvFwSLkAIdRrslxcbcHYIU/2Q7F1wCOWLbaA== xterm-addon-unicode11@0.4.0-beta.3: version "0.4.0-beta.3" resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.27: - version "0.12.0-beta.27" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.27.tgz#afc5bc01d1ef3af9005fb9f6325a4db9c92aa8d9" - integrity sha512-P948trotU8FMHtaA7C2x97VpLq6QLSjO53kWNvONS0/XwEKQBIYCI7Jfri2wcLgfQg6Cn4OQGLoj2YBK3MMyww== +xterm-addon-webgl@0.12.0-beta.29: + version "0.12.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" + integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== -xterm@4.19.0-beta.20: - version "4.19.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.20.tgz#d8e970d8a8460c1d1a5ec9866f78f607a44c1349" - integrity sha512-IYI4ngSWzpV4sJXLWGEDF7vgLuUHn0CUQ42+TGv4H/hCGo4uru4s/D3Yws0ETb3a9VwRpZEPsigULaWTnhFusg== +xterm@4.19.0-beta.25: + version "4.19.0-beta.25" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.25.tgz#38f92d0fef1cfdb290ef8994449a04fa1a8c90a7" + integrity sha512-pDiMWKN1Cj4+X/K9Xegp0SA0ZDEGVqiq7RPSy8oZO2wo2rze1BF20PAZb3/RSp30eY5WyOKilKnck4yNOsPzHw== diff --git a/remote/yarn.lock b/remote/yarn.lock index bd01c5e483d..ac114377345 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -914,10 +914,10 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -xterm-addon-search@0.9.0-beta.18: - version "0.9.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.18.tgz#5317aed1dc747f468ccb7ecd151fb00d82a8a19d" - integrity sha512-SAeA3thc2WJNYXwjOEJFLpZ1ZVOs22RLmz9a6WcrzXkvCjLZRvbRGwX25Ms+Dd7dVDQNbKVUzUJohspP/vYr0Q== +xterm-addon-search@0.9.0-beta.22: + version "0.9.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.22.tgz#18f2eabda82709cdd64dee003879b586869e28a3" + integrity sha512-1I8W0bwjg6bUoNaESKHSkS9BN3Z9Xe44/zbUb6Un+pbb5Nun4LQvFwSLkAIdRrslxcbcHYIU/2Q7F1wCOWLbaA== xterm-addon-serialize@0.7.0-beta.12: version "0.7.0-beta.12" @@ -929,20 +929,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.27: - version "0.12.0-beta.27" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.27.tgz#afc5bc01d1ef3af9005fb9f6325a4db9c92aa8d9" - integrity sha512-P948trotU8FMHtaA7C2x97VpLq6QLSjO53kWNvONS0/XwEKQBIYCI7Jfri2wcLgfQg6Cn4OQGLoj2YBK3MMyww== +xterm-addon-webgl@0.12.0-beta.29: + version "0.12.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" + integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== -xterm-headless@4.19.0-beta.20: - version "4.19.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.20.tgz#9e401920fcc24c2474e0bd45df932c62413594da" - integrity sha512-twp0vCyfdI4wVgDrwxaHk1FtC4UhTNNgbIPT6yVPjICOUkUTOvFjrQCNKHv2uMOJo9uAH2gyOsIqHdEP549rJA== +xterm-headless@4.19.0-beta.25: + version "4.19.0-beta.25" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.25.tgz#a0a1b59f386c44458f06b8ced64e3567371cc983" + integrity sha512-UswSgymk3g9i6XTpFAasnqqIvWhi+AEWT+iO3kkjII6ll+dYEQgeZAv92EnCmeRHp11u5TP+IBAo8jy+aTYbtA== -xterm@4.19.0-beta.20: - version "4.19.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.20.tgz#d8e970d8a8460c1d1a5ec9866f78f607a44c1349" - integrity sha512-IYI4ngSWzpV4sJXLWGEDF7vgLuUHn0CUQ42+TGv4H/hCGo4uru4s/D3Yws0ETb3a9VwRpZEPsigULaWTnhFusg== +xterm@4.19.0-beta.25: + version "4.19.0-beta.25" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.25.tgz#38f92d0fef1cfdb290ef8994449a04fa1a8c90a7" + integrity sha512-pDiMWKN1Cj4+X/K9Xegp0SA0ZDEGVqiq7RPSy8oZO2wo2rze1BF20PAZb3/RSp30eY5WyOKilKnck4yNOsPzHw== yallist@^4.0.0: version "4.0.0" diff --git a/resources/win32/policies/Code.admx b/resources/win32/policies/Code.admx new file mode 100644 index 00000000000..916f503b782 --- /dev/null +++ b/resources/win32/policies/Code.admx @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + none + + + + + manual + + + + + start + + + + + default + + + + + + + diff --git a/resources/win32/policies/en-US/Code.adml b/resources/win32/policies/en-US/Code.adml new file mode 100644 index 00000000000..f79b1778ec4 --- /dev/null +++ b/resources/win32/policies/en-US/Code.adml @@ -0,0 +1,23 @@ + + + + + + + Code - OSS 1.67 or later + Code - OSS + Update + Update Mode + Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service. + Disable updates. + Disable automatic background update checks. Updates will be available if you manually check for updates. + Check for updates only on startup. Disable automatic background update checks. + Enable automatic update checks. Code will check for updates automatically and periodically. + + + + + + + + diff --git a/src/buildfile.js b/src/buildfile.js index 4a174a51436..8c30339da6e 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -76,7 +76,6 @@ exports.code = [ createModuleDescription('vs/code/node/cliProcessMain', ['vs/code/node/cli']), createModuleDescription('vs/code/electron-sandbox/issue/issueReporterMain'), createModuleDescription('vs/code/electron-browser/sharedProcess/sharedProcessMain'), - createModuleDescription('vs/platform/driver/node/driver'), createModuleDescription('vs/code/electron-sandbox/processExplorer/processExplorerMain') ]; diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index b996cc080cb..2a5548664c0 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -470,7 +470,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.focusedItem = (this.focusedItem + 1) % this.viewItems.length; item = this.viewItems[this.focusedItem]; - } while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled()); + } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(); return true; @@ -497,7 +497,7 @@ export class ActionBar extends Disposable implements IActionRunner { this.focusedItem = this.viewItems.length - 1; } item = this.viewItems[this.focusedItem]; - } while (this.focusedItem !== startIndex && this.options.focusOnlyEnabledItems && !item.isEnabled()); + } while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID)); this.updateFocus(true); @@ -525,6 +525,10 @@ export class ActionBar extends Disposable implements IActionRunner { focusItem = false; } + if (actionViewItem.action.id === Separator.ID) { + focusItem = false; + } + if (!focusItem) { this.actionsList.focus({ preventScroll }); this.previouslyFocusedItem = undefined; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 02beee38fd2..8a5ed7180f8 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 79f89381904..0af76e3b6a0 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -88,6 +88,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.setAttribute('aria-haspopup', 'true'); this.element.setAttribute('aria-expanded', 'false'); this.element.title = this._action.label || ''; + this.element.ariaLabel = this._action.label || ''; return null; }; diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 02c98734105..d5811906eda 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -28,6 +28,7 @@ export interface IToolBarOptions { anchorAlignmentProvider?: () => AnchorAlignment; renderDropdownAsChildElement?: boolean; moreIcon?: CSSIcon; + allowContextMenu?: boolean; } /** @@ -63,6 +64,7 @@ export class ToolBar extends Disposable { orientation: options.orientation, ariaLabel: options.ariaLabel, actionRunner: options.actionRunner, + allowContextMenu: options.allowContextMenu, actionViewItemProvider: (action: IAction) => { if (action.id === ToggleMenuAction.ID) { this.toggleMenuActionViewItem = new DropdownMenuActionViewItem( diff --git a/src/vs/base/common/cache.ts b/src/vs/base/common/cache.ts index 5fac0d8f11b..1e675c36e43 100644 --- a/src/vs/base/common/cache.ts +++ b/src/vs/base/common/cache.ts @@ -41,19 +41,40 @@ export class Cache { * Caches just the last value. * The key must be JSON serializable. */ -export class LRUCachedComputed { +export class LRUCachedFunction { private lastCache: TComputed | undefined = undefined; private lastArgKey: string | undefined = undefined; - constructor(private readonly computeFn: (arg: TArg) => TComputed) { + constructor(private readonly fn: (arg: TArg) => TComputed) { } public get(arg: TArg): TComputed { const key = JSON.stringify(arg); if (this.lastArgKey !== key) { this.lastArgKey = key; - this.lastCache = this.computeFn(arg); + this.lastCache = this.fn(arg); } return this.lastCache!; } } + +/** + * Uses an unbounded cache (referential equality) to memoize the results of the given function. +*/ +export class CachedFunction { + private readonly _map = new Map(); + public get cachedValues(): ReadonlyMap { + return this._map; + } + + constructor(private readonly fn: (arg: TArg) => TValue) { } + + public get(arg: TArg): TValue { + if (this._map.has(arg)) { + return this._map.get(arg)!; + } + const value = this.fn(arg); + this._map.set(arg, value); + return value; + } +} diff --git a/src/vs/base/common/errors.ts b/src/vs/base/common/errors.ts index 08528474361..4f397866ae4 100644 --- a/src/vs/base/common/errors.ts +++ b/src/vs/base/common/errors.ts @@ -235,6 +235,10 @@ export class ExpectedError extends Error { export class ErrorNoTelemetry extends Error { public static fromError(err: any): ErrorNoTelemetry { + if (err && err instanceof ErrorNoTelemetry) { + return err; + } + if (err && err instanceof Error) { const result = new ErrorNoTelemetry(); result.name = err.name; @@ -248,3 +252,20 @@ export class ErrorNoTelemetry extends Error { readonly logTelemetry = false; } + +/** + * This error indicates a bug. + * Do not throw this for invalid user input. + * Only catch this error to recover gracefully from bugs. + */ +export class BugIndicatingError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, BugIndicatingError.prototype); + + // Because we know for sure only buggy code throws this, + // we definitely want to break here and fix the bug. + // eslint-disable-next-line no-debugger + debugger; + } +} diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index 0308166c708..d0a8a09aa70 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -472,8 +472,13 @@ function isSeparatorAtPos(value: string, index: number): boolean { case CharCode.Colon: case CharCode.DollarSign: case CharCode.LessThan: + case CharCode.GreaterThan: case CharCode.OpenParen: + case CharCode.CloseParen: case CharCode.OpenSquareBracket: + case CharCode.CloseSquareBracket: + case CharCode.OpenCurlyBrace: + case CharCode.CloseCurlyBrace: return true; case undefined: return false; diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 958eae2d839..0a5039769f5 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { equals } from 'vs/base/common/arrays'; import { isThenable } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; import { isEqualOrParent } from 'vs/base/common/extpath'; @@ -491,7 +492,7 @@ function toRegExp(pattern: string): ParsedStringPattern { /** * Simplified glob matching. Supports a subset of glob patterns: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files) @@ -510,7 +511,7 @@ export function match(arg1: string | IExpression | IRelativePattern, path: strin /** * Simplified glob matching. Supports a subset of glob patterns: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. *.{ts,js} matches all TypeScript and JavaScript files) @@ -797,3 +798,17 @@ function aggregateBasenameMatches(parsedPatterns: Array | undefined, patternsB: Array | undefined): boolean { + return equals(patternsA, patternsB, (a, b) => { + if (typeof a === 'string' && typeof b === 'string') { + return a === b; + } + + if (typeof a !== 'string' && typeof b !== 'string') { + return a.base === b.base && a.pattern === b.pattern; + } + + return false; + }); +} diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index b2e6fef5f92..e0ea1d62c64 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -43,7 +43,6 @@ export interface INodeProcess { versions?: { electron?: string; }; - sandboxed?: boolean; type?: string; cwd: () => string; } @@ -65,7 +64,6 @@ if (typeof globals.vscode !== 'undefined' && typeof globals.vscode.process !== ' const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string'; const isElectronRenderer = isElectronProcess && nodeProcess?.type === 'renderer'; -export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed; interface INavigator { userAgent: string; diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts index db23e105808..3e5c8b7e134 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, isMacintosh } from 'vs/base/common/platform'; /** * Options to be passed to the external program or shell. @@ -124,3 +124,32 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve } }); } + +/** + * Remove dangerous environment variables that have caused crashes + * in forked processes (i.e. in ELECTRON_RUN_AS_NODE processes) + * + * @param env The env object to change + */ +export function removeDangerousEnvVariables(env: IProcessEnvironment | undefined): void { + if (!env) { + return; + } + + // Unset `DEBUG`, as an invalid value might lead to process crashes + // See https://github.com/microsoft/vscode/issues/130072 + delete env['DEBUG']; + + if (isMacintosh) { + // Unset `DYLD_LIBRARY_PATH`, as it leads to process crashes + // See https://github.com/microsoft/vscode/issues/104525 + // See https://github.com/microsoft/vscode/issues/105848 + delete env['DYLD_LIBRARY_PATH']; + } + + if (isLinux) { + // Unset `LD_PRELOAD`, as it might lead to process crashes + // See https://github.com/microsoft/vscode/issues/134177 + delete env['LD_PRELOAD']; + } +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index fd05d5a160c..c66cf41aa6c 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -41,6 +41,7 @@ export interface IProductConfiguration { readonly win32AppUserModelId?: string; readonly win32MutexName?: string; + readonly win32RegValueName?: string; readonly applicationName: string; readonly embedderIdentifier?: string; diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 8aa9e50c9e9..62fddd69f91 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LRUCachedComputed } from 'vs/base/common/cache'; +import { LRUCachedFunction } from 'vs/base/common/cache'; import { CharCode } from 'vs/base/common/charCode'; import { Lazy } from 'vs/base/common/lazy'; import { Constants } from 'vs/base/common/uint'; @@ -1075,7 +1075,7 @@ export class AmbiguousCharacters { ); }); - private static readonly cache = new LRUCachedComputed< + private static readonly cache = new LRUCachedFunction< string[], AmbiguousCharacters >((locales) => { diff --git a/src/vs/base/node/processes.ts b/src/vs/base/node/processes.ts index 4085e82397c..c751a692473 100644 --- a/src/vs/base/node/processes.ts +++ b/src/vs/base/node/processes.ts @@ -87,35 +87,6 @@ function terminateProcess(process: cp.ChildProcess, cwd?: string): Promise { */ quickNavigate?: IQuickNavigateConfiguration; + /** + * Hides the input box from the picker UI. This is typically used + * in combination with quick-navigation where no search UI should + * be presented. + */ + hideInput?: boolean; + /** * a context key to set when this picker is active */ diff --git a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts index 05d5511424c..ea365793c29 100644 --- a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts @@ -8,6 +8,7 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { IListOptions, List } from 'vs/base/browser/ui/list/listWidget'; import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; import { IQuickPick, IQuickPickItem } from 'vs/base/parts/quickinput/common/quickInput'; +import { flakySuite } from 'vs/base/test/common/testUtils'; // Simple promisify of setTimeout function wait(delayMS: number) { @@ -16,7 +17,7 @@ function wait(delayMS: number) { }); } -suite('QuickInput', () => { +flakySuite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 let fixture: HTMLElement, controller: QuickInputController, quickpick: IQuickPick; function getScrollTop(): number { diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index d8cc4c63220..3e25c4097f1 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -23,18 +23,6 @@ return true; } - /** - * @param {string} type - * @returns {type is 'uncaughtException'} - */ - function validateProcessEventType(type) { - if (type !== 'uncaughtException') { - throw new Error(`Unsupported process event '${type}'`); - } - - return true; - } - /** * @param {string} key the name of the process argument to parse * @returns {string | undefined} @@ -264,6 +252,7 @@ get platform() { return process.platform; }, get arch() { return process.arch; }, get env() { return { ...process.env }; }, + get pid() { return process.pid; }, get versions() { return process.versions; }, get type() { return 'renderer'; }, get execPath() { return process.execPath; }, @@ -293,15 +282,11 @@ /** * @param {string} type * @param {Function} callback - * @returns {ISandboxNodeProcess} + * @returns {void} */ on(type, callback) { - if (validateProcessEventType(type)) { - // @ts-ignore - process.on(type, callback); - - return this; - } + // @ts-ignore + process.on(type, callback); } }, diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 698d886ce43..7d9b29a6e3a 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -34,6 +34,11 @@ export interface ISandboxNodeProcess extends INodeProcess { */ readonly sandboxed: boolean; + /** + * The `process.pid` property returns the PID of the process. + */ + readonly pid: number; + /** * A list of versions for the current node.js/electron configuration. */ diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 9ef26186cb9..5fcd4f9f097 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.test.ts @@ -575,4 +575,10 @@ suite('Filters', () => { assert.ok(bScore); assert.ok(aScore[0] === bScore[0]); }); + + test('Unexpected suggest highlighting ignores whole word match in favor of matching first letter#147423', function () { + + assertMatches('i', 'machine/{id}', 'machine/{^id}', fuzzyScore); + assertMatches('ok', 'obobobf{ok}/user', '^obobobf{o^k}/user', fuzzyScore); + }); }); diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index bc70f0fa531..7963c4c4139 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -1066,6 +1066,18 @@ suite('Glob', () => { } }); + test('relative pattern - single star alone', function () { + if (isWindows) { + let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo\\something\\Program.cs', pattern: '*' }; + assertGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\something\\Program.cs'); + assertNoGlobMatch(p, 'C:\\DNXConsoleApp\\foo\\Program.cs'); + } else { + let p: glob.IRelativePattern = { base: '/DNXConsoleApp/foo/something/Program.cs', pattern: '*' }; + assertGlobMatch(p, '/DNXConsoleApp/foo/something/Program.cs'); + assertNoGlobMatch(p, '/DNXConsoleApp/foo/Program.cs'); + } + }); + test('relative pattern - ignores case on macOS/Windows', function () { if (isWindows) { let p: glob.IRelativePattern = { base: 'C:\\DNXConsoleApp\\foo', pattern: 'something/*.cs' }; @@ -1114,4 +1126,18 @@ suite('Glob', () => { assert.strictEqual('**/*.js', await parsedExpression('test.js', undefined, hasSibling)); }); + + test('patternsEquals', () => { + assert.ok(glob.patternsEquals(['a'], ['a'])); + assert.ok(!glob.patternsEquals(['a'], ['b'])); + + assert.ok(glob.patternsEquals(['a', 'b', 'c'], ['a', 'b', 'c'])); + assert.ok(!glob.patternsEquals(['1', '2'], ['1', '3'])); + + assert.ok(glob.patternsEquals([{ base: 'a', pattern: '*' }, 'b', 'c'], [{ base: 'a', pattern: '*' }, 'b', 'c'])); + + assert.ok(glob.patternsEquals(undefined, undefined)); + assert.ok(!glob.patternsEquals(undefined, ['b'])); + assert.ok(!glob.patternsEquals(['a'], undefined)); + }); }); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index f48212b5add..19fa65f56dc 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -37,7 +37,6 @@ import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/el import { IDiagnosticsService } from 'vs/platform/diagnostics/common/diagnostics'; import { DiagnosticsMainService, IDiagnosticsMainService } from 'vs/platform/diagnostics/electron-main/diagnosticsMainService'; import { DialogMainService, IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogMainService'; -import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; import { EncryptionMainService } from 'vs/platform/encryption/node/encryptionMainService'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; @@ -523,14 +522,6 @@ export class CodeApplication extends Disposable { // Services const appInstantiationService = await this.initServices(machineId, sharedProcess, sharedProcessReady); - // Create driver - if (this.environmentMainService.driverHandle) { - const server = await serveDriver(mainProcessElectronServer, this.environmentMainService.driverHandle, appInstantiationService); - - this.logService.info('Driver started at:', this.environmentMainService.driverHandle); - this._register(server); - } - // Setup Auth Handler this._register(appInstantiationService.createInstance(ProxyAuthHandler)); @@ -1150,7 +1141,7 @@ export class CodeApplication extends Disposable { // Initialize update service const updateService = accessor.get(IUpdateService); if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) { - updateService.initialize(); + await updateService.initialize(); } // Start to fetch shell environment (if needed) after window has opened diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 489de25caf0..68ac4e98056 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -190,7 +190,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { let activeIndentEndLineNumber = 0; let activeIndentLevel = 0; - if (this._bracketPairGuideOptions.highlightActiveIndentation && activeCursorPosition) { + if (this._bracketPairGuideOptions.highlightActiveIndentation !== false && activeCursorPosition) { const activeIndentInfo = this._context.viewModel.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber); activeIndentStartLineNumber = activeIndentInfo.startLineNumber; activeIndentEndLineNumber = activeIndentInfo.endLineNumber; @@ -213,7 +213,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const indentGuide = (indentLvl - 1) * indentSize + 1; const isActive = // Disable active indent guide if there are bracket guides. - bracketGuidesInLine.length === 0 && + (this._bracketPairGuideOptions.highlightActiveIndentation === 'always' || bracketGuidesInLine.length === 0) && activeIndentStartLineNumber <= lineNumber && lineNumber <= activeIndentEndLineNumber && indentLvl === activeIndentLevel; diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index febb7dac4d6..889bd36b2d8 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2565,16 +2565,16 @@ class EditorInlayHints extends BaseEditorOption { const background = theme.getColor(editorBackground); if (background) { - collector.addRule(`.monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { background-color: ${background}; }`); + collector.addRule(`.monaco-editor, .monaco-editor-background { background-color: ${background}; }`); + } + + const lineHighlight = theme.getColor(editorLineHighlight); + const imeBackground = (lineHighlight && !lineHighlight.isTransparent() ? lineHighlight : background); + if (imeBackground) { + collector.addRule(`.monaco-editor .inputarea.ime-input { background-color: ${imeBackground}; }`); } const foreground = theme.getColor(editorForeground); diff --git a/src/vs/editor/common/languageSelector.ts b/src/vs/editor/common/languageSelector.ts index 2ef0be7ee10..64a8440059f 100644 --- a/src/vs/editor/common/languageSelector.ts +++ b/src/vs/editor/common/languageSelector.ts @@ -87,7 +87,7 @@ export function score(selector: LanguageSelector | undefined, candidateUri: URI, if (notebookType) { if (notebookType === candidateNotebookType) { ret = 10; - } else if (notebookType === '*') { + } else if (notebookType === '*' && candidateNotebookType !== undefined) { ret = Math.max(ret, 5); } else { return 0; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 3659925bf75..fedc753fb66 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -833,6 +833,10 @@ export interface InlineCompletion { export interface InlineCompletions { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider { diff --git a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts index 4229b5aa3a0..79f92c6ed52 100644 --- a/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts +++ b/src/vs/editor/common/languages/supports/languageBracketsConfiguration.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CachedFunction } from 'vs/base/common/cache'; +import { BugIndicatingError } from 'vs/base/common/errors'; import { LanguageConfiguration } from 'vs/editor/common/languages/languageConfiguration'; /** @@ -35,14 +37,14 @@ export class LanguageBracketsConfiguration { brackets = []; } - const openingBracketInfos = new LazyMap((bracket: string) => { + const openingBracketInfos = new CachedFunction((bracket: string) => { const closing = new Set(); return { info: new OpeningBracketKind(this, bracket, closing), closing, }; }); - const closingBracketInfos = new LazyMap((bracket: string) => { + const closingBracketInfos = new CachedFunction((bracket: string) => { const opening = new Set(); return { info: new ClosingBracketKind(this, bracket, opening), @@ -58,8 +60,8 @@ export class LanguageBracketsConfiguration { closing.opening.add(opening.info); } - this._openingBrackets = new Map([...openingBracketInfos.innerMap].map(([k, v]) => [k, v.info])); - this._closingBrackets = new Map([...closingBracketInfos.innerMap].map(([k, v]) => [k, v.info])); + this._openingBrackets = new Map([...openingBracketInfos.cachedValues].map(([k, v]) => [k, v.info])); + this._closingBrackets = new Map([...closingBracketInfos.cachedValues].map(([k, v]) => [k, v.info])); } /** @@ -151,38 +153,3 @@ export class ClosingBracketKind extends BracketKindBase { return [...this.closedBrackets]; } } - -// Utilities - -/** - * This error indicates a bug. - */ -class BugIndicatingError extends Error { - constructor(message: string) { - super(message); - Object.setPrototypeOf(this, BugIndicatingError.prototype); - - // Because we know for sure only buggy code throws this, - // we definitely want to break here and fix the bug. - // eslint-disable-next-line no-debugger - debugger; - } -} - -class LazyMap { - private readonly _map = new Map(); - public get innerMap(): ReadonlyMap { - return this._map; - } - - constructor(private readonly initialize: (key: TKey) => TValue) { } - - public get(key: TKey): TValue { - if (this._map.has(key)) { - return this._map.get(key)!; - } - const value = this.initialize(key); - this._map.set(key, value); - return value; - } -} diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts index 9dc3e7e0b40..280cb3b8deb 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets.ts @@ -114,12 +114,8 @@ export class LanguageAgnosticBracketTokens { } public didLanguageChange(languageId: string): boolean { - const existing = this.languageIdToBracketTokens.get(languageId); - if (!existing) { - return false; - } - const newRegExpStr = BracketTokens.createFromLanguage(this.getLanguageConfiguration(languageId), this.denseKeyProvider).getRegExpStr(); - return existing.getRegExpStr() !== newRegExpStr; + // Report a change whenever the language configuration updates. + return this.languageIdToBracketTokens.has(languageId); } getSingleLanguageBracketTokens(languageId: string): BracketTokens { diff --git a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts index baee24e0c7b..2e28c88ff5e 100644 --- a/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.ts @@ -259,7 +259,9 @@ export class ReferenceWidget extends peekView.PeekViewWidget { } override show(where: IRange) { - this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth); + setTimeout(() => { + this.editor.revealRangeInCenterIfOutsideViewport({ ...where, endLineNumber: where.startLineNumber + 1, endColumn: 1 }, ScrollType.Smooth); + }); super.show(where, this.layoutData.heightInLines || 18); } diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 85d07db70d7..7adeb41f7f1 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -258,8 +258,8 @@ export class InlayHintsController implements IEditorContribution { } // mouse gestures + this._sessionDisposables.add(this._installDblClickGesture(() => scheduler.schedule(0))); this._sessionDisposables.add(this._installLinkGesture()); - this._sessionDisposables.add(this._installDblClickGesture()); this._sessionDisposables.add(this._installContextMenu()); } @@ -329,7 +329,7 @@ export class InlayHintsController implements IEditorContribution { return Array.from(lineHints); } - private _installDblClickGesture(): IDisposable { + private _installDblClickGesture(updateInlayHints: Function): IDisposable { return this._editor.onMouseUp(async e => { if (e.event.detail !== 2) { return; @@ -343,6 +343,7 @@ export class InlayHintsController implements IEditorContribution { if (isNonEmptyArray(part.item.hint.textEdits)) { const edits = part.item.hint.textEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)); this._editor.executeEdits('inlayHint.default', edits); + updateInlayHints(); } }); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts index 7ab26922483..6a2fe0eb945 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextHoverParticipant.ts @@ -20,6 +20,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; +import { Command } from 'vs/editor/common/languages'; export class InlineCompletionsHover implements IHoverPart { constructor( @@ -39,6 +40,10 @@ export class InlineCompletionsHover implements IHoverPart { public hasMultipleSuggestions(): Promise { return this.controller.hasMultipleInlineCompletions(); } + + public get commands(): Command[] { + return this.controller.activeModel?.activeInlineCompletionsModel?.completionSession.value?.commands || []; + } } export class InlineCompletionsHoverParticipant implements IEditorHoverParticipant { @@ -100,6 +105,7 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan this.renderScreenReaderText(context, part, disposableStore); } + // TODO@hediet: deprecate MenuId.InlineCompletionsActions const menu = disposableStore.add(this._menuService.createMenu( MenuId.InlineCompletionsActions, this._contextKeyService @@ -131,6 +137,14 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan } }); + for (const command of part.commands) { + context.statusBar.addAction({ + label: command.title, + commandId: command.id, + run: () => this._commandService.executeCommand(command.id, ...(command.arguments || [])) + }); + } + for (const [_, group] of menu.getActions()) { for (const action of group) { if (action instanceof MenuItemAction) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts index ee134296862..d6095291d4c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextWidget.ts @@ -81,7 +81,7 @@ export class GhostTextWidget extends Disposable { if (!this.editor.hasModel() || !ghostText || this.disposed) { this.partsWidget.clear(); this.additionalLinesWidget.clear(); - this.replacementDecoration.setDecorations([]); + this.replacementDecoration.clear(); return; } @@ -213,8 +213,14 @@ class DisposableDecorations { public setDecorations(decorations: IModelDeltaDecoration[]): void { this.decorationIds = this.editor.deltaDecorations(this.decorationIds, decorations); } - public dispose(): void { + + public clear() { this.editor.deltaDecorations(this.decorationIds, []); + this.decorationIds = []; + } + + public dispose(): void { + this.clear(); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index fb8393f665a..e7870b9c77d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -15,7 +15,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; +import { Command, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionTriggerKind } from 'vs/editor/common/languages'; import { BaseGhostTextWidgetModel, GhostText, GhostTextReplacement, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/browser/ghostText'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { inlineSuggestCommitId } from 'vs/editor/contrib/inlineCompletions/browser/consts'; @@ -403,10 +403,14 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { if (!currentCompletion) { return undefined; } + const cursorPosition = this.editor.getPosition(); + if (currentCompletion.range.getEndPosition().isBefore(cursorPosition)) { + return undefined; + } const mode = this.editor.getOptions().get(EditorOption.inlineSuggest).mode; - const ghostText = inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, this.editor.getPosition()); + const ghostText = inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, cursorPosition); if (ghostText) { if (ghostText.isEmpty()) { return undefined; @@ -543,6 +547,11 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { this.onDidChangeEmitter.fire(); } + + public get commands(): Command[] { + const lists = new Set(this.cache.value?.completions.map(c => c.inlineCompletion.sourceInlineCompletions) || []); + return [...lists].flatMap(l => l.commands || []); + } } export class UpdateOperation implements IDisposable { @@ -560,6 +569,7 @@ export class UpdateOperation implements IDisposable { */ export class SynchronizedInlineCompletionsCache extends Disposable { public readonly completions: readonly CachedInlineCompletion[]; + private isDisposing = false; constructor( completionsSource: TrackedInlineCompletions, @@ -579,6 +589,7 @@ export class SynchronizedInlineCompletionsCache extends Disposable { })) ); this._register(toDisposable(() => { + this.isDisposing = true; editor.deltaDecorations(decorationIds, []); })); @@ -591,7 +602,11 @@ export class SynchronizedInlineCompletionsCache extends Disposable { this._register(completionsSource); } - public updateRanges() { + public updateRanges(): void { + if (this.isDisposing) { + return; + } + let hasChanged = false; const model = this.editor.getModel(); for (const c of this.completions) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts index 5a97c173751..0391d402bc1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetPreviewModel.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -import { InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; +import { CompletionItemKind, InlineCompletionTriggerKind, SelectedSuggestionInfo } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { SharedInlineCompletionCache } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextModel'; import { BaseGhostTextWidgetModel, GhostText } from './ghostText'; @@ -99,6 +99,17 @@ export class SuggestWidgetPreviewModel extends BaseGhostTextWidgetModel { const position = this.editor.getPosition(); + if ( + state.selectedItem.isSnippetText || + state.selectedItem.completionItemKind === CompletionItemKind.Snippet || + state.selectedItem.completionItemKind === CompletionItemKind.File || + state.selectedItem.completionItemKind === CompletionItemKind.Folder + ) { + // Don't ask providers for these types of suggestions. + this.cache.clear(); + return; + } + const promise = createCancelablePromise(async token => { let result: TrackedInlineCompletions; try { diff --git a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts index 2a323d76679..7220dbf1a4f 100644 --- a/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts +++ b/src/vs/editor/contrib/markdownRenderer/browser/markdownRenderer.ts @@ -23,6 +23,7 @@ export interface IMarkdownRenderResult extends IDisposable { export interface IMarkdownRendererOptions { editor?: ICodeEditor; codeBlockFontFamily?: string; + codeBlockFontSize?: string; } /** @@ -93,6 +94,10 @@ export class MarkdownRenderer { element.style.fontFamily = this._options.codeBlockFontFamily; } + if (this._options.codeBlockFontSize !== undefined) { + element.style.fontSize = this._options.codeBlockFontSize; + } + return element; }, asyncRenderCallback: () => this._onDidRenderAsync.fire(), diff --git a/src/vs/editor/contrib/snippet/browser/snippetController2.ts b/src/vs/editor/contrib/snippet/browser/snippetController2.ts index b1b4a6a1dd5..8d90c8e30e9 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetController2.ts @@ -340,6 +340,7 @@ export function performSnippetEdit(editor: ICodeEditor, edit: SnippetTextEdit) { if (!controller) { return false; } + editor.focus(); editor.setSelection(edit.range); controller.insert(edit.snippet); return controller.isInSnippet(); diff --git a/src/vs/editor/contrib/snippet/browser/snippetParser.ts b/src/vs/editor/contrib/snippet/browser/snippetParser.ts index b896cff1f74..01baba9eeff 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetParser.ts @@ -395,8 +395,7 @@ export class FormatString extends Marker { return value; } return match.map(word => { - return word.charAt(0).toUpperCase() - + word.substr(1).toLowerCase(); + return word.charAt(0).toUpperCase() + word.substr(1); }) .join(''); } @@ -408,11 +407,9 @@ export class FormatString extends Marker { } return match.map((word, index) => { if (index === 0) { - return word.toLowerCase(); - } else { - return word.charAt(0).toUpperCase() - + word.substr(1).toLowerCase(); + return word.charAt(0).toLowerCase() + word.substr(1); } + return word.charAt(0).toUpperCase() + word.substr(1); }) .join(''); } diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index 26cef12d7b3..320aa6af88e 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -656,8 +656,14 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo'); assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('snake_AndPascalCase'), 'SnakeAndPascalCase'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('kebab-AndPascalCase'), 'KebabAndPascalCase'); + assert.strictEqual(new FormatString(1, 'pascalcase').resolve('_justPascalCase'), 'JustPascalCase'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('snake_AndCamelCase'), 'snakeAndCamelCase'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('kebab-AndCamelCase'), 'kebabAndCamelCase'); + assert.strictEqual(new FormatString(1, 'camelcase').resolve('_JustCamelCase'), 'justCamelCase'); assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input'); // if diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 04e6390ba9c..8ac347d22b9 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -223,6 +223,7 @@ export class SuggestWidget implements IDisposable { alwaysConsumeMouseWheel: true, useShadows: false, mouseSupport: false, + multipleSelectionSupport: false, accessibilityProvider: { getRole: () => 'option', getWidgetAriaLabel: () => nls.localize('suggest', "Suggest"), diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 3e53572dd10..81a9950b5e4 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -85,6 +85,7 @@ import { MarkerService } from 'vs/platform/markers/common/markerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { staticObservableValue } from 'vs/base/common/observableValue'; import 'vs/editor/common/services/languageFeaturesService'; @@ -640,7 +641,7 @@ class StandaloneResourcePropertiesService implements ITextResourcePropertiesServ class StandaloneTelemetryService implements ITelemetryService { declare readonly _serviceBrand: undefined; - public telemetryLevel = TelemetryLevel.NONE; + public telemetryLevel = staticObservableValue(TelemetryLevel.NONE); public sendErrorTelemetry = false; public setEnabled(value: boolean): void { diff --git a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts index 3fce47976a9..d10e1ccafb9 100644 --- a/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts +++ b/src/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.ts @@ -7,6 +7,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { IStandaloneThemeService } from 'vs/editor/standalone/common/standaloneTheme'; import { ToggleHighContrastNLS } from 'vs/editor/common/standaloneStrings'; +import { isHighContrast } from 'vs/platform/theme/common/theme'; class ToggleHighContrast extends EditorAction { @@ -24,9 +25,9 @@ class ToggleHighContrast extends EditorAction { public run(accessor: ServicesAccessor, editor: ICodeEditor): void { const standaloneThemeService = accessor.get(IStandaloneThemeService); - if (this._originalThemeName) { + if (isHighContrast(standaloneThemeService.getColorTheme().type)) { // We must toggle back to the integrator's theme - standaloneThemeService.setTheme(this._originalThemeName); + standaloneThemeService.setTheme(this._originalThemeName || 'vs'); this._originalThemeName = null; } else { this._originalThemeName = standaloneThemeService.getColorTheme().themeName; diff --git a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts index c8d33c13f95..3359f968f5b 100644 --- a/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts +++ b/src/vs/editor/test/browser/view/minimapCharRenderer.test.ts @@ -58,6 +58,7 @@ suite('MinimapCharRenderer', () => { function createFakeImageData(width: number, height: number): ImageData { return { + colorSpace: 'srgb', width: width, height: height, data: new Uint8ClampedArray(width * height * Constants.RGBA_CHANNELS_CNT) diff --git a/src/vs/editor/test/common/modes/languageSelector.test.ts b/src/vs/editor/test/common/modes/languageSelector.test.ts index e2d59071382..53cba5e386e 100644 --- a/src/vs/editor/test/common/modes/languageSelector.test.ts +++ b/src/vs/editor/test/common/modes/languageSelector.test.ts @@ -131,4 +131,23 @@ suite('LanguageSelector', function () { let value = score(selector, URI.file('/home/user/Desktop/test.json'), 'json', true, undefined); assert.strictEqual(value, 10); }); + + test('NotebookType without notebook', function () { + let obj = { + uri: URI.parse('file:///my/file.bat'), + langId: 'bat', + }; + + let value = score({ + language: 'bat', + notebookType: 'xxx' + }, obj.uri, obj.langId, true, undefined); + assert.strictEqual(value, 0); + + value = score({ + language: 'bat', + notebookType: '*' + }, obj.uri, obj.langId, true, undefined); + assert.strictEqual(value, 0); + }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index c3dab93ceb4..3995d59d6e7 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4032,7 +4032,7 @@ declare namespace monaco.editor { * Enable highlighting of the active indent guide. * Defaults to true. */ - highlightActiveIndentation?: boolean; + highlightActiveIndentation?: boolean | 'always'; } /** @@ -6332,6 +6332,10 @@ declare namespace monaco.languages { export interface InlineCompletions { readonly items: readonly TItem[]; + /** + * A list of commands associated with the inline completions of this list. + */ + readonly commands?: Command[]; } export interface InlineCompletionsProvider { diff --git a/src/vs/platform/credentials/common/credentialsMainService.ts b/src/vs/platform/credentials/common/credentialsMainService.ts index 6bed808b144..912cb0fb5c6 100644 --- a/src/vs/platform/credentials/common/credentialsMainService.ts +++ b/src/vs/platform/credentials/common/credentialsMainService.ts @@ -37,11 +37,22 @@ export abstract class BaseCredentialsMainService extends Disposable implements I public abstract getSecretStoragePrefix(): Promise; protected abstract withKeytar(): Promise; + /** + * An optional method that subclasses can implement to assist in surfacing + * Keytar load errors to the user in a friendly way. + */ + protected abstract surfaceKeytarLoadError?: (err: any) => void; //#endregion async getPassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return null; + } const password = await keytar.getPassword(service, account); if (password) { @@ -70,7 +81,14 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + this.surfaceKeytarLoadError?.(e); + throw e; + } + const MAX_SET_ATTEMPTS = 3; // Sometimes Keytar has a problem talking to the keychain on the OS. To be more resilient, we retry a few times. @@ -119,7 +137,13 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async deletePassword(service: string, account: string): Promise { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + this.surfaceKeytarLoadError?.(e); + throw e; + } const didDelete = await keytar.deletePassword(service, account); if (didDelete) { @@ -130,13 +154,25 @@ export abstract class BaseCredentialsMainService extends Disposable implements I } async findPassword(service: string): Promise { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return null; + } return keytar.findPassword(service); } async findCredentials(service: string): Promise> { - const keytar = await this.withKeytar(); + let keytar: KeytarModule; + try { + keytar = await this.withKeytar(); + } catch (e) { + // for get operations, we don't want to surface errors to the user + return []; + } return keytar.findCredentials(service); } diff --git a/src/vs/platform/credentials/electron-main/credentialsMainService.ts b/src/vs/platform/credentials/electron-main/credentialsMainService.ts index 0bf232d2153..5d782da694d 100644 --- a/src/vs/platform/credentials/electron-main/credentialsMainService.ts +++ b/src/vs/platform/credentials/electron-main/credentialsMainService.ts @@ -36,14 +36,13 @@ export class CredentialsNativeMainService extends BaseCredentialsMainService { return this._keytarCache; } - try { - this._keytarCache = await import('keytar'); - // Try using keytar to see if it throws or not. - await this._keytarCache.findCredentials('test-keytar-loads'); - } catch (e) { - this.windowsMainService.sendToFocused('vscode:showCredentialsError', e.message ?? e); - throw e; - } + this._keytarCache = await import('keytar'); + // Try using keytar to see if it throws or not. + await this._keytarCache.findCredentials('test-keytar-loads'); return this._keytarCache; } + + protected override surfaceKeytarLoadError = (err: any) => { + this.windowsMainService.sendToFocused('vscode:showCredentialsError', err.message ?? err); + }; } diff --git a/src/vs/platform/credentials/node/credentialsMainService.ts b/src/vs/platform/credentials/node/credentialsMainService.ts index f00f3532d00..cc2156d22cd 100644 --- a/src/vs/platform/credentials/node/credentialsMainService.ts +++ b/src/vs/platform/credentials/node/credentialsMainService.ts @@ -10,6 +10,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { BaseCredentialsMainService, KeytarModule } from 'vs/platform/credentials/common/credentialsMainService'; export class CredentialsWebMainService extends BaseCredentialsMainService { + // Since we fallback to the in-memory credentials provider, we do not need to surface any Keytar load errors + // to the user. + protected surfaceKeytarLoadError?: (err: any) => void; constructor( @ILogService logService: ILogService, diff --git a/src/vs/platform/driver/browser/driver.ts b/src/vs/platform/driver/browser/driver.ts index c57855c3828..c16032c953f 100644 --- a/src/vs/platform/driver/browser/driver.ts +++ b/src/vs/platform/driver/browser/driver.ts @@ -205,6 +205,10 @@ export class BrowserWindowDriver implements IWindowDriver { throw new Error('Method not implemented.'); } + + async exitApplication(): Promise { + // No-op in web + } } export function registerWindowDriver(): void { diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index 264ed83ec8b..1c593be20d1 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - // !! Do not remove the following START and END markers, they are parsed by the smoketest build //*START @@ -19,14 +17,7 @@ export interface IElement { } export interface ILocaleInfo { - /** - * The UI language used. - */ language: string; - - /** - * The requested locale - */ locale?: string; } @@ -36,27 +27,6 @@ export interface ILocalizedStrings { find: string; } -export interface IDriver { - readonly _serviceBrand: undefined; - - getWindowIds(): Promise; - startTracing(windowId: number, name: string): Promise; - stopTracing(windowId: number, name: string, persist: boolean): Promise; - exitApplication(): Promise; - dispatchKeybinding(windowId: number, keybinding: string): Promise; - click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - setValue(windowId: number, selector: string, text: string): Promise; - getTitle(windowId: number): Promise; - isActiveElement(windowId: number, selector: string): Promise; - getElements(windowId: number, selector: string, recursive?: boolean): Promise; - getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }>; - typeInEditor(windowId: number, selector: string, text: string): Promise; - getTerminalBuffer(windowId: number, selector: string): Promise; - writeInTerminal(windowId: number, selector: string, text: string): Promise; - getLocaleInfo(windowId: number): Promise; - getLocalizedStrings(windowId: number): Promise; -} - export interface IWindowDriver { click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; setValue(selector: string, text: string): Promise; @@ -69,13 +39,6 @@ export interface IWindowDriver { writeInTerminal(selector: string, text: string): Promise; getLocaleInfo(): Promise; getLocalizedStrings(): Promise; + exitApplication(): Promise; } //*END - -export const ID = 'driverService'; -export const IDriver = createDecorator(ID); - -export interface IWindowDriverRegistry { - registerWindowDriver(windowId: number): Promise; - reloadWindowDriver(windowId: number): Promise; -} diff --git a/src/vs/platform/driver/common/driverIpc.ts b/src/vs/platform/driver/common/driverIpc.ts deleted file mode 100644 index 6d5ed5e55c4..00000000000 --- a/src/vs/platform/driver/common/driverIpc.ts +++ /dev/null @@ -1,101 +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 { Event } from 'vs/base/common/event'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IElement, ILocaleInfo, ILocalizedStrings as ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; - -export class WindowDriverChannel implements IServerChannel { - - constructor(private driver: IWindowDriver) { } - - listen(_: unknown, event: string): Event { - throw new Error(`No event found: ${event}`); - } - - call(_: unknown, command: string, arg?: any): Promise { - switch (command) { - case 'click': return this.driver.click(arg[0], arg[1], arg[2]); - case 'setValue': return this.driver.setValue(arg[0], arg[1]); - case 'getTitle': return this.driver.getTitle(); - case 'isActiveElement': return this.driver.isActiveElement(arg); - case 'getElements': return this.driver.getElements(arg[0], arg[1]); - case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); - case 'getLocaleInfo': return this.driver.getLocaleInfo(); - case 'getLocalizedStrings': return this.driver.getLocalizedStrings(); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export class WindowDriverChannelClient implements IWindowDriver { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - click(selector: string, xoffset?: number, yoffset?: number): Promise { - return this.channel.call('click', [selector, xoffset, yoffset]); - } - - setValue(selector: string, text: string): Promise { - return this.channel.call('setValue', [selector, text]); - } - - getTitle(): Promise { - return this.channel.call('getTitle'); - } - - isActiveElement(selector: string): Promise { - return this.channel.call('isActiveElement', selector); - } - - getElements(selector: string, recursive: boolean): Promise { - return this.channel.call('getElements', [selector, recursive]); - } - - getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - return this.channel.call('getElementXY', [selector, xoffset, yoffset]); - } - - typeInEditor(selector: string, text: string): Promise { - return this.channel.call('typeInEditor', [selector, text]); - } - - getTerminalBuffer(selector: string): Promise { - return this.channel.call('getTerminalBuffer', selector); - } - - writeInTerminal(selector: string, text: string): Promise { - return this.channel.call('writeInTerminal', [selector, text]); - } - - getLocaleInfo(): Promise { - return this.channel.call('getLocaleInfo'); - } - - getLocalizedStrings(): Promise { - return this.channel.call('getLocalizedStrings'); - } -} - -export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - registerWindowDriver(windowId: number): Promise { - return this.channel.call('registerWindowDriver', windowId); - } - - reloadWindowDriver(windowId: number): Promise { - return this.channel.call('reloadWindowDriver', windowId); - } -} diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts deleted file mode 100644 index c071bf0cf1c..00000000000 --- a/src/vs/platform/driver/electron-main/driver.ts +++ /dev/null @@ -1,246 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { timeout } from 'vs/base/common/async'; -import { Emitter, Event } from 'vs/base/common/event'; -import { KeybindingParser } from 'vs/base/common/keybindingParser'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { SimpleKeybinding, ScanCodeBinding } from 'vs/base/common/keybindings'; -import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; -import { OS } from 'vs/base/common/platform'; -import { IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriver, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; -import { WindowDriverChannelClient } from 'vs/platform/driver/common/driverIpc'; -import { DriverChannel, WindowDriverRegistryChannel } from 'vs/platform/driver/node/driver'; -import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; -import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; -import { IFileService } from 'vs/platform/files/common/files'; -import { URI } from 'vs/base/common/uri'; -import { join } from 'vs/base/common/path'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { ILogService } from 'vs/platform/log/common/log'; - -function isSilentKeyCode(keyCode: KeyCode) { - return keyCode < KeyCode.Digit0; -} - -export class Driver implements IDriver, IWindowDriverRegistry { - - declare readonly _serviceBrand: undefined; - - private registeredWindowIds = new Set(); - private reloadingWindowIds = new Set(); - private readonly onDidReloadingChange = new Emitter(); - - constructor( - private windowServer: IPCServer, - @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, - @IFileService private readonly fileService: IFileService, - @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, - @ILogService private readonly logService: ILogService - ) { } - - async registerWindowDriver(windowId: number): Promise { - this.logService.info(`[driver] registerWindowDriver(${windowId})`); - - this.registeredWindowIds.add(windowId); - this.reloadingWindowIds.delete(windowId); - this.onDidReloadingChange.fire(); - } - - async reloadWindowDriver(windowId: number): Promise { - this.logService.info(`[driver] reloadWindowDriver(${windowId})`); - - this.reloadingWindowIds.add(windowId); - } - - async getWindowIds(): Promise { - const windowIds = this.windowsMainService.getWindows() - .map(window => window.id) - .filter(windowId => this.registeredWindowIds.has(windowId) && !this.reloadingWindowIds.has(windowId)); - - return windowIds; - } - - - async startTracing(windowId: number, name: string): Promise { - // ignore - tracing is not implemented yet - } - - async stopTracing(windowId: number, name: string, persist: boolean): Promise { - if (!persist) { - return; - } - - const raw = await this.capturePage(windowId); - const buffer = Buffer.from(raw, 'base64'); - - await this.fileService.writeFile(URI.file(join(this.environmentMainService.logsPath, `${name}.png`)), VSBuffer.wrap(buffer)); - } - - private async capturePage(windowId: number): Promise { - const window = this.windowsMainService.getWindowById(windowId) ?? this.windowsMainService.getLastActiveWindow(); // fallback to active window to ensure we capture window - if (!window?.win) { - throw new Error('Invalid window'); - } - - const webContents = window.win.webContents; - const image = await webContents.capturePage(); - return image.toPNG().toString('base64'); - } - - async exitApplication(): Promise { - this.logService.info(`[driver] exitApplication()`); - - this.lifecycleMainService.quit(); - - return process.pid; - } - - async dispatchKeybinding(windowId: number, keybinding: string): Promise { - await this.whenUnfrozen(windowId); - - const parts = KeybindingParser.parseUserBinding(keybinding); - - for (let part of parts) { - await this._dispatchKeybinding(windowId, part); - } - } - - private async _dispatchKeybinding(windowId: number, keybinding: SimpleKeybinding | ScanCodeBinding): Promise { - if (keybinding instanceof ScanCodeBinding) { - throw new Error('ScanCodeBindings not supported'); - } - - const window = this.windowsMainService.getWindowById(windowId); - if (!window?.win) { - throw new Error('Invalid window'); - } - const webContents = window.win.webContents; - const noModifiedKeybinding = new SimpleKeybinding(false, false, false, false, keybinding.keyCode); - const resolvedKeybinding = new USLayoutResolvedKeybinding(noModifiedKeybinding.toChord(), OS); - const keyCode = resolvedKeybinding.getElectronAccelerator(); - - const modifiers: string[] = []; - - if (keybinding.ctrlKey) { - modifiers.push('ctrl'); - } - - if (keybinding.metaKey) { - modifiers.push('meta'); - } - - if (keybinding.shiftKey) { - modifiers.push('shift'); - } - - if (keybinding.altKey) { - modifiers.push('alt'); - } - - webContents.sendInputEvent({ type: 'keyDown', keyCode, modifiers } as any); - - if (!isSilentKeyCode(keybinding.keyCode)) { - webContents.sendInputEvent({ type: 'char', keyCode, modifiers } as any); - } - - webContents.sendInputEvent({ type: 'keyUp', keyCode, modifiers } as any); - - await timeout(100); - } - - async click(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.click(selector, xoffset, yoffset); - } - - async setValue(windowId: number, selector: string, text: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.setValue(selector, text); - } - - async getTitle(windowId: number): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getTitle(); - } - - async isActiveElement(windowId: number, selector: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.isActiveElement(selector); - } - - async getElements(windowId: number, selector: string, recursive: boolean): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getElements(selector, recursive); - } - - async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getElementXY(selector, xoffset, yoffset); - } - - async typeInEditor(windowId: number, selector: string, text: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.typeInEditor(selector, text); - } - - async getTerminalBuffer(windowId: number, selector: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getTerminalBuffer(selector); - } - - async writeInTerminal(windowId: number, selector: string, text: string): Promise { - const windowDriver = await this.getWindowDriver(windowId); - await windowDriver.writeInTerminal(selector, text); - } - - async getLocaleInfo(windowId: number): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getLocaleInfo(); - } - - async getLocalizedStrings(windowId: number): Promise { - const windowDriver = await this.getWindowDriver(windowId); - return await windowDriver.getLocalizedStrings(); - } - - private async getWindowDriver(windowId: number): Promise { - await this.whenUnfrozen(windowId); - - const id = `window:${windowId}`; - const router = new StaticRouter(ctx => ctx === id); - const windowDriverChannel = this.windowServer.getChannel('windowDriver', router); - return new WindowDriverChannelClient(windowDriverChannel); - } - - private async whenUnfrozen(windowId: number): Promise { - while (this.reloadingWindowIds.has(windowId)) { - await Event.toPromise(this.onDidReloadingChange.event); - } - } -} - -export async function serve( - windowServer: IPCServer, - handle: string, - instantiationService: IInstantiationService -): Promise { - const driver = instantiationService.createInstance(Driver, windowServer); - - const windowDriverRegistryChannel = new WindowDriverRegistryChannel(driver); - windowServer.registerChannel('windowDriverRegistry', windowDriverRegistryChannel); - - const server = await serveNet(handle); - const channel = new DriverChannel(driver); - server.registerChannel('driver', channel); - - return combinedDisposable(server, windowServer); -} diff --git a/src/vs/platform/driver/electron-sandbox/driver.ts b/src/vs/platform/driver/electron-sandbox/driver.ts index bc2e4c9ae38..fb9b9a596ff 100644 --- a/src/vs/platform/driver/electron-sandbox/driver.ts +++ b/src/vs/platform/driver/electron-sandbox/driver.ts @@ -3,16 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout } from 'vs/base/common/async'; -import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { BrowserWindowDriver } from 'vs/platform/driver/browser/driver'; -import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/common/driverIpc'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; -import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; interface INativeWindowDriverHelper { - exitApplication(): Promise; + exitApplication(): Promise; } class NativeWindowDriver extends BrowserWindowDriver { @@ -21,7 +15,7 @@ class NativeWindowDriver extends BrowserWindowDriver { super(); } - exitApplication(): Promise { + override exitApplication(): Promise { return this.helper.exitApplication(); } } @@ -29,50 +23,3 @@ class NativeWindowDriver extends BrowserWindowDriver { export function registerWindowDriver(helper: INativeWindowDriverHelper): void { Object.assign(window, { driver: new NativeWindowDriver(helper) }); } - -class LegacyNativeWindowDriver extends BrowserWindowDriver { - - constructor( - @INativeHostService private readonly nativeHostService: INativeHostService - ) { - super(); - } - - override click(selector: string, xoffset?: number, yoffset?: number): Promise { - const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; - - return this.doClick(selector, 1, offset); - } - - private async doClick(selector: string, clickCount: number, offset?: { x: number; y: number }): Promise { - const { x, y } = await this._getElementXY(selector, offset); - - await this.nativeHostService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); - await timeout(10); - - await this.nativeHostService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any); - await timeout(100); - } -} - -/** - * Old school window driver that is implemented by us - * from the main process. - * - * @deprecated - */ -export async function registerLegacyWindowDriver(accessor: ServicesAccessor, windowId: number): Promise { - const instantiationService = accessor.get(IInstantiationService); - const mainProcessService = accessor.get(IMainProcessService); - - const windowDriver = instantiationService.createInstance(LegacyNativeWindowDriver); - const windowDriverChannel = new WindowDriverChannel(windowDriver); - mainProcessService.registerChannel('windowDriver', windowDriverChannel); - - const windowDriverRegistryChannel = mainProcessService.getChannel('windowDriverRegistry'); - const windowDriverRegistry = new WindowDriverRegistryChannelClient(windowDriverRegistryChannel); - - await windowDriverRegistry.registerWindowDriver(windowId); - - return toDisposable(() => windowDriverRegistry.reloadWindowDriver(windowId)); -} diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts deleted file mode 100644 index 1542e503f1d..00000000000 --- a/src/vs/platform/driver/node/driver.ts +++ /dev/null @@ -1,138 +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 { Event } from 'vs/base/common/event'; -import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/common/ipc.net'; -import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { IDriver, IElement, ILocaleInfo, ILocalizedStrings, IWindowDriverRegistry } from 'vs/platform/driver/common/driver'; - -export class DriverChannel implements IServerChannel { - - constructor(private driver: IDriver) { } - - listen(_: unknown, event: string): Event { - throw new Error('No event found'); - } - - call(_: unknown, command: string, arg?: any): Promise { - switch (command) { - case 'getWindowIds': return this.driver.getWindowIds(); - case 'startTracing': return this.driver.startTracing(arg[0], arg[1]); - case 'stopTracing': return this.driver.stopTracing(arg[0], arg[1], arg[2]); - case 'exitApplication': return this.driver.exitApplication(); - case 'dispatchKeybinding': return this.driver.dispatchKeybinding(arg[0], arg[1]); - case 'click': return this.driver.click(arg[0], arg[1], arg[2], arg[3]); - case 'setValue': return this.driver.setValue(arg[0], arg[1], arg[2]); - case 'getTitle': return this.driver.getTitle(arg[0]); - case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); - case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); - case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); - case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); - case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); - case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); - case 'getLocaleInfo': return this.driver.getLocaleInfo(arg); - case 'getLocalizedStrings': return this.driver.getLocalizedStrings(arg); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export class DriverChannelClient implements IDriver { - - declare readonly _serviceBrand: undefined; - - constructor(private channel: IChannel) { } - - getWindowIds(): Promise { - return this.channel.call('getWindowIds'); - } - - startTracing(windowId: number, name: string): Promise { - return this.channel.call('startTracing', [windowId, name]); - } - - stopTracing(windowId: number, name: string, persist: boolean): Promise { - return this.channel.call('stopTracing', [windowId, name, persist]); - } - - exitApplication(): Promise { - return this.channel.call('exitApplication'); - } - - dispatchKeybinding(windowId: number, keybinding: string): Promise { - return this.channel.call('dispatchKeybinding', [windowId, keybinding]); - } - - click(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise { - return this.channel.call('click', [windowId, selector, xoffset, yoffset]); - } - - setValue(windowId: number, selector: string, text: string): Promise { - return this.channel.call('setValue', [windowId, selector, text]); - } - - getTitle(windowId: number): Promise { - return this.channel.call('getTitle', [windowId]); - } - - isActiveElement(windowId: number, selector: string): Promise { - return this.channel.call('isActiveElement', [windowId, selector]); - } - - getElements(windowId: number, selector: string, recursive: boolean): Promise { - return this.channel.call('getElements', [windowId, selector, recursive]); - } - - getElementXY(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<{ x: number; y: number }> { - return this.channel.call('getElementXY', [windowId, selector, xoffset, yoffset]); - } - - typeInEditor(windowId: number, selector: string, text: string): Promise { - return this.channel.call('typeInEditor', [windowId, selector, text]); - } - - getTerminalBuffer(windowId: number, selector: string): Promise { - return this.channel.call('getTerminalBuffer', [windowId, selector]); - } - - writeInTerminal(windowId: number, selector: string, text: string): Promise { - return this.channel.call('writeInTerminal', [windowId, selector, text]); - } - - getLocaleInfo(windowId: number): Promise { - return this.channel.call('getLocaleInfo', windowId); - } - - getLocalizedStrings(windowId: number): Promise { - return this.channel.call('getLocalizedStrings', windowId); - } -} - -export class WindowDriverRegistryChannel implements IServerChannel { - - constructor(private registry: IWindowDriverRegistry) { } - - listen(_: unknown, event: string): Event { - throw new Error(`Event not found: ${event}`); - } - - call(_: unknown, command: string, arg?: any): Promise { - switch (command) { - case 'registerWindowDriver': return this.registry.registerWindowDriver(arg); - case 'reloadWindowDriver': return this.registry.reloadWindowDriver(arg); - } - - throw new Error(`Call not found: ${command}`); - } -} - -export async function connect(handle: string): Promise<{ client: Client; driver: IDriver }> { - const client = await connectNet(handle, 'driverClient'); - const channel = client.getChannel('driver'); - const driver = new DriverChannelClient(channel); - return { client, driver }; -} diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index ccac10545b5..b2b4500a614 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -79,10 +79,6 @@ export interface NativeParsedArgs { 'max-memory'?: string; 'file-write'?: boolean; 'file-chmod'?: boolean; - /** - * @deprecated use `enable-smoke-test-driver` - */ - 'driver'?: string; 'enable-smoke-test-driver'?: boolean; 'remote'?: string; 'force'?: boolean; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 28a690c69bb..56531a3a784 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -132,9 +132,6 @@ export interface INativeEnvironmentService extends IEnvironmentService { extensionsDownloadPath: string; builtinExtensionsPath: string; - // --- smoke test support - driverHandle?: string; - // --- use keytar for credentials disableKeytar?: boolean; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index ac02eadcbdb..fcea3f58023 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -230,8 +230,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron get crashReporterId(): string | undefined { return this.args['crash-reporter-id']; } get crashReporterDirectory(): string | undefined { return this.args['crash-reporter-directory']; } - get driverHandle(): string | undefined { return this.args['driver']; } - @memoize get telemetryLogResource(): URI { return URI.file(join(this.logsPath, 'telemetry.log')); } get disableTelemetry(): boolean { return !!this.args['disable-telemetry']; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 1d7aadc1f14..062ce48a580 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -98,7 +98,6 @@ export const OPTIONS: OptionDescriptions> = { 'inspect-brk-search': { type: 'string', deprecates: ['debugBrkSearch'] }, 'export-default-configuration': { type: 'string' }, 'install-source': { type: 'string' }, - 'driver': { type: 'string' }, 'enable-smoke-test-driver': { type: 'boolean' }, 'logExtensionHostCommunication': { type: 'boolean' }, 'skip-release-notes': { type: 'boolean' }, diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 81f5764cb47..2e52feaa54f 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Promises, Queue } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStringDictionary } from 'vs/base/common/collections'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { getErrorMessage } from 'vs/base/common/errors'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import * as semver from 'vs/base/common/semver/semver'; import { isBoolean, isUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; -import { IFile, zip } from 'vs/base/node/zip'; +import { extract, ExtractError, IFile, zip } from 'vs/base/node/zip'; import * as nls from 'vs/nls'; import { IDownloadService } from 'vs/platform/download/common/download'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -24,12 +27,12 @@ import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGalleryExtension, IGalleryMetadata, ILocalExtension, InstallOperation, InstallOptions, InstallVSIXOptions, Metadata } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { areSameExtensions, computeTargetPlatform, ExtensionKey, getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; -import { ExtensionsScanner } from 'vs/platform/extensionManagement/node/extensionsScanner'; import { ExtensionsWatcher } from 'vs/platform/extensionManagement/node/extensionsWatcher'; import { ExtensionType, IExtensionManifest, TargetPlatform } from 'vs/platform/extensions/common/extensions'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -187,6 +190,272 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } +class ExtensionsScanner extends Disposable { + + private readonly uninstalledPath: string; + private readonly uninstalledFileLimiter: Queue; + + constructor( + private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, + @IFileService private readonly fileService: IFileService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath; + this.uninstalledFileLimiter = new Queue(); + } + + async cleanUp(): Promise { + await this.removeUninstalledExtensions(); + await this.removeOutdatedExtensions(); + } + + async scanExtensions(type: ExtensionType | null): Promise { + const scannedOptions: ScanOptions = { includeInvalid: true }; + let scannedExtensions: IScannedExtension[] = []; + if (type === null || type === ExtensionType.System) { + scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions)); + } else if (type === ExtensionType.User) { + scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions)); + } + scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } + + async scanUserExtensions(excludeOutdated: boolean): Promise { + const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); + return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); + } + + async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise { + const folderName = extensionKey.toString(); + const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`); + const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName); + + try { + await pfs.Promises.rm(extensionPath); + } catch (error) { + throw new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete); + } + + await this.extractAtLocation(extensionKey, zipPath, tempPath, token); + await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() }); + + try { + await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); + this.logService.info('Renamed to', extensionPath); + } catch (error) { + try { + await pfs.Promises.rm(tempPath); + } catch (e) { /* ignore */ } + if (error.code === 'ENOTEMPTY') { + this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); + } else { + this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath); + throw error; + } + } + + return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User); + } + + async updateMetadata(local: ILocalExtension, metadata: Partial): Promise { + await this.extensionsScannerService.updateMetadata(local.location, metadata); + return this.scanLocalExtension(local.location, local.type); + } + + getUninstalledExtensions(): Promise> { + return this.withUninstalledExtensions(); + } + + async setUninstalled(...extensions: ILocalExtension[]): Promise { + const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); + await this.withUninstalledExtensions(uninstalled => { + extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); + }); + } + + async setInstalled(extensionKey: ExtensionKey): Promise { + await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); + const userExtensions = await this.scanUserExtensions(true); + const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; + if (!localExtension) { + return null; + } + return this.updateMetadata(localExtension, { installedTimestamp: Date.now() }); + } + + async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { + this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); + await pfs.Promises.rm(extension.location.fsPath); + this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); + } + + async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); + } + + private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { + return this.uninstalledFileLimiter.queue(async () => { + let raw: string | undefined; + try { + raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + let uninstalled = {}; + if (raw) { + try { + uninstalled = JSON.parse(raw); + } catch (e) { /* ignore */ } + } + + if (updateFn) { + updateFn(uninstalled); + if (Object.keys(uninstalled).length) { + await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); + } else { + await pfs.Promises.rm(this.uninstalledPath); + } + } + + return uninstalled; + }); + } + + private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { + this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); + + // Clean the location + try { + await pfs.Promises.rm(location); + } catch (e) { + throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete); + } + + try { + await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); + this.logService.info(`Extracted extension to ${location}:`, identifier.id); + } catch (e) { + try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } + let errorCode = ExtensionManagementErrorCode.Extract; + if (e instanceof ExtractError) { + if (e.type === 'CorruptZip') { + errorCode = ExtensionManagementErrorCode.CorruptZip; + } else if (e.type === 'Incomplete') { + errorCode = ExtensionManagementErrorCode.IncompleteZip; + } + } + throw new ExtensionManagementError(e.message, errorCode); + } + } + + private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { + try { + await pfs.Promises.rename(extractPath, renamePath); + } catch (error) { + if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); + return this.rename(identifier, extractPath, renamePath, retryUntil); + } + throw new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); + } + } + + private async scanLocalExtension(location: URI, type: ExtensionType): Promise { + const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); + if (scannedExtension) { + return this.toLocalExtension(scannedExtension); + } + throw new Error(nls.localize('cannot read', "Cannot read the extension from {0}", location.path)); + } + + private async toLocalExtension(extension: IScannedExtension): Promise { + const stat = await this.fileService.resolve(extension.location); + let readmeUrl: URI | undefined; + let changelogUrl: URI | undefined; + if (stat.children) { + readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; + changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; + } + return { + identifier: extension.identifier, + type: extension.type, + isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin, + location: extension.location, + manifest: extension.manifest, + targetPlatform: extension.targetPlatform, + validations: extension.validations, + isValid: extension.isValid, + readmeUrl, + changelogUrl, + publisherDisplayName: extension.metadata?.publisherDisplayName || null, + publisherId: extension.metadata?.publisherId || null, + isMachineScoped: !!extension.metadata?.isMachineScoped, + isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, + preRelease: !!extension.metadata?.preRelease, + installedTimestamp: extension.metadata?.installedTimestamp, + updated: !!extension.metadata?.updated, + }; + } + private async removeUninstalledExtensions(): Promise { + const uninstalled = await this.getUninstalledExtensions(); + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const installed: Set = new Set(); + for (const e of extensions) { + if (!uninstalled[ExtensionKey.create(e).toString()]) { + installed.add(e.identifier.id.toLowerCase()); + } + } + const byExtension = groupByExtension(extensions, e => e.identifier); + await Promises.settled(byExtension.map(async e => { + const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; + if (!installed.has(latest.identifier.id.toLowerCase())) { + await this.beforeRemovingExtension(await this.toLocalExtension(latest)); + } + })); + const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); + await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); + } + + private async removeOutdatedExtensions(): Promise { + const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions + const toRemove: IScannedExtension[] = []; + + // Outdated extensions + const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); + const byExtension = groupByExtension(extensions, e => e.identifier); + toRemove.push(...byExtension.map(p => p.sort((a, b) => { + const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); + if (vcompare !== 0) { + return vcompare; + } + if (a.targetPlatform === targetPlatform) { + return -1; + } + return 1; + }).slice(1)).flat()); + + await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); + } + + private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { + const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; + if (errors.length === 1) { + return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); + } + return errors.reduce((previousValue: Error, currentValue: Error | string) => { + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); + }, new Error('')); + } + +} + abstract class AbstractInstallExtensionTask extends AbstractExtensionTask implements IInstallExtensionTask { protected _operation = InstallOperation.Install; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts deleted file mode 100644 index 0e57ea12488..00000000000 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ /dev/null @@ -1,292 +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 { flatten } from 'vs/base/common/arrays'; -import { Promises, Queue } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { getErrorMessage } from 'vs/base/common/errors'; -import { Disposable } from 'vs/base/common/lifecycle'; -import * as path from 'vs/base/common/path'; -import { isWindows } from 'vs/base/common/platform'; -import { joinPath } from 'vs/base/common/resources'; -import * as semver from 'vs/base/common/semver/semver'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import * as pfs from 'vs/base/node/pfs'; -import { extract, ExtractError } from 'vs/base/node/zip'; -import { localize } from 'vs/nls'; -import { ExtensionManagementError, ExtensionManagementErrorCode, Metadata, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionKey, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { IExtensionsScannerService, IScannedExtension, ScanOptions } from 'vs/platform/extensionManagement/common/extensionsScannerService'; -import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; - -export class ExtensionsScanner extends Disposable { - - private readonly uninstalledPath: string; - private readonly uninstalledFileLimiter: Queue; - - constructor( - private readonly beforeRemovingExtension: (e: ILocalExtension) => Promise, - @IFileService private readonly fileService: IFileService, - @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, - @ILogService private readonly logService: ILogService, - ) { - super(); - this.uninstalledPath = joinPath(this.extensionsScannerService.userExtensionsLocation, '.obsolete').fsPath; - this.uninstalledFileLimiter = new Queue(); - } - - async cleanUp(): Promise { - await this.removeUninstalledExtensions(); - await this.removeOutdatedExtensions(); - } - - async scanExtensions(type: ExtensionType | null): Promise { - const scannedOptions: ScanOptions = { includeInvalid: true }; - let scannedExtensions: IScannedExtension[] = []; - if (type === null || type === ExtensionType.System) { - scannedExtensions.push(...await this.extensionsScannerService.scanAllExtensions(scannedOptions)); - } else if (type === ExtensionType.User) { - scannedExtensions.push(...await this.extensionsScannerService.scanUserExtensions(scannedOptions)); - } - scannedExtensions = type !== null ? scannedExtensions.filter(r => r.type === type) : scannedExtensions; - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); - } - - async scanUserExtensions(excludeOutdated: boolean): Promise { - const scannedExtensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: !excludeOutdated, includeInvalid: true }); - return Promise.all(scannedExtensions.map(extension => this.toLocalExtension(extension))); - } - - async extractUserExtension(extensionKey: ExtensionKey, zipPath: string, metadata: Metadata | undefined, token: CancellationToken): Promise { - const folderName = extensionKey.toString(); - const tempPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, `.${generateUuid()}`); - const extensionPath = path.join(this.extensionsScannerService.userExtensionsLocation.fsPath, folderName); - - try { - await pfs.Promises.rm(extensionPath); - } catch (error) { - throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, extensionKey.id), ExtensionManagementErrorCode.Delete); - } - - await this.extractAtLocation(extensionKey, zipPath, tempPath, token); - await this.extensionsScannerService.updateMetadata(URI.file(tempPath), { ...metadata, installedTimestamp: Date.now() }); - - try { - await this.rename(extensionKey, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); - this.logService.info('Renamed to', extensionPath); - } catch (error) { - try { - await pfs.Promises.rm(tempPath); - } catch (e) { /* ignore */ } - if (error.code === 'ENOTEMPTY') { - this.logService.info(`Rename failed because extension was installed by another source. So ignoring renaming.`, extensionKey.id); - } else { - this.logService.info(`Rename failed because of ${getErrorMessage(error)}. Deleted from extracted location`, tempPath); - throw error; - } - } - - return this.scanLocalExtension(URI.file(extensionPath), ExtensionType.User); - } - - async updateMetadata(local: ILocalExtension, metadata: Partial): Promise { - await this.extensionsScannerService.updateMetadata(local.location, metadata); - return this.scanLocalExtension(local.location, local.type); - } - - getUninstalledExtensions(): Promise> { - return this.withUninstalledExtensions(); - } - - async setUninstalled(...extensions: ILocalExtension[]): Promise { - const extensionKeys: ExtensionKey[] = extensions.map(e => ExtensionKey.create(e)); - await this.withUninstalledExtensions(uninstalled => { - extensionKeys.forEach(extensionKey => uninstalled[extensionKey.toString()] = true); - }); - } - - async setInstalled(extensionKey: ExtensionKey): Promise { - await this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionKey.toString()]); - const userExtensions = await this.scanUserExtensions(true); - const localExtension = userExtensions.find(i => ExtensionKey.create(i).equals(extensionKey)) || null; - if (!localExtension) { - return null; - } - return this.updateMetadata(localExtension, { installedTimestamp: Date.now() }); - } - - async removeExtension(extension: ILocalExtension | IScannedExtension, type: string): Promise { - this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - await pfs.Promises.rm(extension.location.fsPath); - this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); - } - - async removeUninstalledExtension(extension: ILocalExtension | IScannedExtension): Promise { - await this.removeExtension(extension, 'uninstalled'); - await this.withUninstalledExtensions(uninstalled => delete uninstalled[ExtensionKey.create(extension).toString()]); - } - - private async withUninstalledExtensions(updateFn?: (uninstalled: IStringDictionary) => void): Promise> { - return this.uninstalledFileLimiter.queue(async () => { - let raw: string | undefined; - try { - raw = await pfs.Promises.readFile(this.uninstalledPath, 'utf8'); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - - let uninstalled = {}; - if (raw) { - try { - uninstalled = JSON.parse(raw); - } catch (e) { /* ignore */ } - } - - if (updateFn) { - updateFn(uninstalled); - if (Object.keys(uninstalled).length) { - await pfs.Promises.writeFile(this.uninstalledPath, JSON.stringify(uninstalled)); - } else { - await pfs.Promises.rm(this.uninstalledPath); - } - } - - return uninstalled; - }); - } - - private async extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); - - // Clean the location - try { - await pfs.Promises.rm(location); - } catch (e) { - throw new ExtensionManagementError(this.joinErrors(e).message, ExtensionManagementErrorCode.Delete); - } - - try { - await extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token); - this.logService.info(`Extracted extension to ${location}:`, identifier.id); - } catch (e) { - try { await pfs.Promises.rm(location); } catch (e) { /* Ignore */ } - let errorCode = ExtensionManagementErrorCode.Extract; - if (e instanceof ExtractError) { - if (e.type === 'CorruptZip') { - errorCode = ExtensionManagementErrorCode.CorruptZip; - } else if (e.type === 'Incomplete') { - errorCode = ExtensionManagementErrorCode.IncompleteZip; - } - } - throw new ExtensionManagementError(e.message, errorCode); - } - } - - private async rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { - try { - await pfs.Promises.rename(extractPath, renamePath); - } catch (error) { - if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { - this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); - return this.rename(identifier, extractPath, renamePath, retryUntil); - } - throw new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || ExtensionManagementErrorCode.Rename); - } - } - - private async scanLocalExtension(location: URI, type: ExtensionType): Promise { - const scannedExtension = await this.extensionsScannerService.scanExistingExtension(location, type, { includeInvalid: true }); - if (scannedExtension) { - return this.toLocalExtension(scannedExtension); - } - throw new Error(localize('cannot read', "Cannot read the extension from {0}", location.path)); - } - - private async toLocalExtension(extension: IScannedExtension): Promise { - const stat = await this.fileService.resolve(extension.location); - let readmeUrl: URI | undefined; - let changelogUrl: URI | undefined; - if (stat.children) { - readmeUrl = stat.children.find(({ name }) => /^readme(\.txt|\.md|)$/i.test(name))?.resource; - changelogUrl = stat.children.find(({ name }) => /^changelog(\.txt|\.md|)$/i.test(name))?.resource; - } - return { - identifier: extension.identifier, - type: extension.type, - isBuiltin: extension.isBuiltin || !!extension.metadata?.isBuiltin, - location: extension.location, - manifest: extension.manifest, - targetPlatform: extension.targetPlatform, - validations: extension.validations, - isValid: extension.isValid, - readmeUrl, - changelogUrl, - publisherDisplayName: extension.metadata?.publisherDisplayName || null, - publisherId: extension.metadata?.publisherId || null, - isMachineScoped: !!extension.metadata?.isMachineScoped, - isPreReleaseVersion: !!extension.metadata?.isPreReleaseVersion, - preRelease: !!extension.metadata?.preRelease, - installedTimestamp: extension.metadata?.installedTimestamp, - updated: !!extension.metadata?.updated, - }; - } - private async removeUninstalledExtensions(): Promise { - const uninstalled = await this.getUninstalledExtensions(); - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions - const installed: Set = new Set(); - for (const e of extensions) { - if (!uninstalled[ExtensionKey.create(e).toString()]) { - installed.add(e.identifier.id.toLowerCase()); - } - } - const byExtension = groupByExtension(extensions, e => e.identifier); - await Promises.settled(byExtension.map(async e => { - const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; - if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.beforeRemovingExtension(await this.toLocalExtension(latest)); - } - })); - const toRemove = extensions.filter(e => uninstalled[ExtensionKey.create(e).toString()]); - await Promises.settled(toRemove.map(e => this.removeUninstalledExtension(e))); - } - - private async removeOutdatedExtensions(): Promise { - const extensions = await this.extensionsScannerService.scanUserExtensions({ includeAllVersions: true, includeUninstalled: true, includeInvalid: true }); // All user extensions - const toRemove: IScannedExtension[] = []; - - // Outdated extensions - const targetPlatform = await this.extensionsScannerService.getTargetPlatform(); - const byExtension = groupByExtension(extensions, e => e.identifier); - toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => { - const vcompare = semver.rcompare(a.manifest.version, b.manifest.version); - if (vcompare !== 0) { - return vcompare; - } - if (a.targetPlatform === targetPlatform) { - return -1; - } - return 1; - }).slice(1)))); - - await Promises.settled(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); - } - - private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { - const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; - if (errors.length === 1) { - return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); - } - return errors.reduce((previousValue: Error, currentValue: Error | string) => { - return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); - }, new Error('')); - } - -} diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 028d33a315c..47e7c042f8f 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -7,7 +7,7 @@ import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/comm import { CancellationToken } from 'vs/base/common/cancellation'; import { ErrorNoTelemetry } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; -import { IExpression } from 'vs/base/common/glob'; +import { IExpression, IRelativePattern } from 'vs/base/common/glob'; import { IDisposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { sep } from 'vs/base/common/path'; @@ -437,7 +437,7 @@ export interface IWatchOptions { * watching. If not provided, all paths are considered for * events. */ - includes?: string[]; + includes?: Array; } export const enum FileSystemProviderCapabilities { diff --git a/src/vs/platform/files/common/watcher.ts b/src/vs/platform/files/common/watcher.ts index 78614c324dc..5a2a7cfd786 100644 --- a/src/vs/platform/files/common/watcher.ts +++ b/src/vs/platform/files/common/watcher.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IRelativePattern } from 'vs/base/common/glob'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { isLinux } from 'vs/base/common/platform'; import { URI as uri } from 'vs/base/common/uri'; @@ -31,7 +32,7 @@ interface IWatchRequest { * watching. If not provided, all paths are considered for * events. */ - includes?: string[]; + includes?: Array; } export interface INonRecursiveWatchRequest extends IWatchRequest { diff --git a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts index df6bd3f016c..a737ca54510 100644 --- a/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts +++ b/src/vs/platform/files/node/watcher/nodejs/nodejsWatcher.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equals } from 'vs/base/common/arrays'; import { Event, Emitter } from 'vs/base/common/event'; +import { patternsEquals } from 'vs/base/common/glob'; import { Disposable } from 'vs/base/common/lifecycle'; import { isLinux } from 'vs/base/common/platform'; import { IDiskFileChange, ILogMessage, INonRecursiveWatchRequest, INonRecursiveWatcher } from 'vs/platform/files/common/watcher'; @@ -50,12 +50,12 @@ export class NodeJSWatcher extends Disposable implements INonRecursiveWatcher { } // Re-watch path if excludes or includes have changed - return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes); + return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes); }); // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { - return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && equals(normalizedRequest.excludes, request.excludes) && equals(normalizedRequest.includes, request.includes)); + return !normalizedRequests.find(normalizedRequest => normalizedRequest.path === request.path && patternsEquals(normalizedRequest.excludes, request.excludes) && patternsEquals(normalizedRequest.includes, request.includes)); }).map(({ request }) => request.path); // Logging diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 5b0d051b30e..eab00c852b2 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -11,7 +11,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter } from 'vs/base/common/event'; import { isEqualOrParent, randomPath } from 'vs/base/common/extpath'; -import { parse, ParsedPattern } from 'vs/base/common/glob'; +import { parse, ParsedPattern, patternsEquals } from 'vs/base/common/glob'; import { Disposable } from 'vs/base/common/lifecycle'; import { TernarySearchTree } from 'vs/base/common/map'; import { normalizeNFC } from 'vs/base/common/normalization'; @@ -22,7 +22,6 @@ import { realcaseSync, realpathSync } from 'vs/base/node/extpath'; import { NodeJSFileWatcherLibrary } from 'vs/platform/files/node/watcher/nodejs/nodejsWatcherLib'; import { FileChangeType } from 'vs/platform/files/common/files'; import { IDiskFileChange, ILogMessage, coalesceEvents, IRecursiveWatchRequest, IRecursiveWatcher } from 'vs/platform/files/common/watcher'; -import { equals } from 'vs/base/common/arrays'; export interface IParcelWatcherInstance { @@ -129,15 +128,15 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Re-watch path if excludes/includes have changed or polling interval - return !equals(watcher.request.excludes, request.excludes) || !equals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; + return !patternsEquals(watcher.request.excludes, request.excludes) || !patternsEquals(watcher.request.includes, request.includes) || watcher.request.pollingInterval !== request.pollingInterval; }); // Gather paths that we should stop watching const pathsToStopWatching = Array.from(this.watchers.values()).filter(({ request }) => { return !normalizedRequests.find(normalizedRequest => { return normalizedRequest.path === request.path && - equals(normalizedRequest.excludes, request.excludes) && - equals(normalizedRequest.includes, request.includes) && + patternsEquals(normalizedRequest.excludes, request.excludes) && + patternsEquals(normalizedRequest.includes, request.includes) && normalizedRequest.pollingInterval === request.pollingInterval; }); @@ -393,14 +392,16 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return; } - // Check for excludes - const rawEvents = this.handleExcludeIncludes(parcelEvents, excludes, includes); - // Normalize events: handle NFC normalization and symlinks - const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(rawEvents, watcher.request, realPathDiffers, realPathLength); + // It is important to do this before checking for includes + // and excludes to check on the original path. + const { events: normalizedEvents, rootDeleted } = this.normalizeEvents(parcelEvents, watcher.request, realPathDiffers, realPathLength); + + // Check for excludes + const includedEvents = this.handleExcludeIncludes(normalizedEvents, excludes, includes); // Coalesce events: merge events of same kind - const coalescedEvents = coalesceEvents(normalizedEvents); + const coalescedEvents = coalesceEvents(includedEvents); // Filter events: check for specific events we want to exclude const filteredEvents = this.filterEvents(coalescedEvents, watcher.request, rootDeleted); @@ -495,7 +496,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { return { realPath, realPathDiffers, realPathLength }; } - private normalizeEvents(events: IDiskFileChange[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: IDiskFileChange[]; rootDeleted: boolean } { + private normalizeEvents(events: parcelWatcher.Event[], request: IRecursiveWatchRequest, realPathDiffers: boolean, realPathLength: number): { events: parcelWatcher.Event[]; rootDeleted: boolean } { let rootDeleted = false; for (const event of events) { @@ -519,7 +520,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { } // Check for root deleted - if (event.path === request.path && event.type === FileChangeType.DELETED) { + if (event.path === request.path && event.type === 'delete') { rootDeleted = true; } } @@ -672,7 +673,7 @@ export class ParcelWatcher extends Disposable implements IRecursiveWatcher { try { const realpath = realpathSync(request.path); if (realpath === request.path) { - this.warn(`ignoring a path for watching who's parent is already watched: ${request.path}`); + this.trace(`ignoring a path for watching who's parent is already watched: ${request.path}`); continue; // path is not a symbolic link or similar } diff --git a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts index d617ce2b2e4..6bacce6495c 100644 --- a/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/nodejsWatcher.integrationTest.ts @@ -392,6 +392,12 @@ import { NodeJSWatcher } from 'vs/platform/files/node/watcher/nodejs/nodejsWatch return basicCrudTest(join(testDir, 'files-includes.txt')); }); + test('includes are supported (folder watch, relative pattern)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'files-includes.txt' }], recursive: false }]); + + return basicCrudTest(join(testDir, 'files-includes.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (folder watch)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); diff --git a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts index 5529a525e0c..0dcb643870c 100644 --- a/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts +++ b/src/vs/platform/files/test/node/parcelWatcher.integrationTest.ts @@ -450,6 +450,12 @@ import { ltrim } from 'vs/base/common/strings'; return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); }); + test('includes are supported (relative pattern)', async function () { + await watcher.watch([{ path: testDir, excludes: [], includes: [{ base: testDir, pattern: 'deep/newFile.txt' }], recursive: true }]); + + return basicCrudTest(join(testDir, 'deep', 'newFile.txt')); + }); + (isWindows /* windows: cannot create file symbolic link without elevated context */ ? test.skip : test)('symlink support (root)', async function () { const link = join(testDir, 'deep-linked'); const linkTarget = join(testDir, 'deep'); diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index f0d57b347d5..bce9d83386f 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -153,6 +153,7 @@ export interface ICommonNativeHostService { // Connectivity resolveProxy(url: string): Promise; + findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise; // Registry (windows only) windowsGetStringRegKey(hive: 'HKEY_CURRENT_USER' | 'HKEY_LOCAL_MACHINE' | 'HKEY_CLASSES_ROOT' | 'HKEY_USERS' | 'HKEY_CURRENT_CONFIG', path: string, name: string): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index ac91aa14727..0233e52e050 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -19,6 +19,7 @@ import { URI } from 'vs/base/common/uri'; import { realpath } from 'vs/base/node/extpath'; import { virtualMachineHint } from 'vs/base/node/id'; import { Promises, SymlinkSupport } from 'vs/base/node/pfs'; +import { findFreePort } from 'vs/base/node/ports'; import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { localize } from 'vs/nls'; import { ISerializableCommandAction } from 'vs/platform/action/common/action'; @@ -736,6 +737,10 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + findFreePort(windowId: number | undefined, startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise { + return findFreePort(startPort, giveUpAfter, timeout, stride); + } + //#endregion diff --git a/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts new file mode 100644 index 00000000000..81f17b25ed2 --- /dev/null +++ b/src/vs/platform/remote/test/electron-sandbox/remoteAuthorityResolverService.test.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-sandbox/remoteAuthorityResolverService'; + +suite('RemoteAuthorityResolverService', () => { + test('issue #147318: RemoteAuthorityResolverError keeps the same type', async () => { + const service = new RemoteAuthorityResolverService(); + const result = service.resolveAuthority('test+x'); + service._setResolvedAuthorityError('test+x', new RemoteAuthorityResolverError('something', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable)); + try { + await result; + assert.fail(); + } catch (err) { + assert.strictEqual(RemoteAuthorityResolverError.isTemporarilyNotAvailable(err), true); + } + }); +}); diff --git a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts index f0af8c6648e..165268c5e12 100644 --- a/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts +++ b/src/vs/platform/sharedProcess/electron-browser/sharedProcessWorkerMain.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepClone } from 'vs/base/common/objects'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { hash, ISharedProcessWorkerConfiguration, ISharedProcessWorkerProcessExit } from 'vs/platform/sharedProcess/common/sharedProcessWorkerService'; import { SharedProcessWorkerMessages, ISharedProcessToWorkerMessage, ISharedProcessWorkerEnvironment, IWorkerToSharedProcessMessage } from 'vs/platform/sharedProcess/electron-browser/sharedProcessWorker'; diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 83f76d7f402..f7046a4d756 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -60,7 +60,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { } private async onWindowConnection(e: IpcMainEvent, nonce: string): Promise { - this.logService.info('SharedProcess: on vscode:createSharedProcessMessageChannel'); + this.logService.trace('SharedProcess: on vscode:createSharedProcessMessageChannel'); // release barrier if this is the first window connection if (!this.firstWindowConnectionBarrier.isOpen()) { @@ -175,7 +175,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Overall signal that the shared process window was loaded and // all services within have been created. this._whenReady = new Promise(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=init-done', () => { - this.logService.info('SharedProcess: Overall ready'); + this.logService.trace('SharedProcess: Overall ready'); resolve(); })); @@ -200,7 +200,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { // Wait for window indicating that IPC connections are accepted await new Promise(resolve => validatedIpcMain.once('vscode:shared-process->electron-main=ipc-ready', () => { - this.logService.info('SharedProcess: IPC ready'); + this.logService.trace('SharedProcess: IPC ready'); resolve(); })); diff --git a/src/vs/platform/storage/common/storageIpc.ts b/src/vs/platform/storage/common/storageIpc.ts index e6916d0c230..5774cb82085 100644 --- a/src/vs/platform/storage/common/storageIpc.ts +++ b/src/vs/platform/storage/common/storageIpc.ts @@ -86,8 +86,9 @@ class GlobalStorageDatabaseClient extends BaseStorageDatabaseClient implements I async close(): Promise { // The global storage database is shared across all instances so - // we do not await it. However we dispose the listener for external - // changes because we no longer interested int it. + // we do not close it from the window. However we dispose the + // listener for external changes because we no longer interested in it. + this.dispose(); } } @@ -101,9 +102,12 @@ class WorkspaceStorageDatabaseClient extends BaseStorageDatabaseClient implement } async close(): Promise { - const serializableRequest: ISerializableUpdateRequest = { workspace: this.workspace }; - return this.channel.call('close', serializableRequest); + // The workspace storage database is only used in this instance + // but we do not need to close it from here, the main process + // can take care of that. + + this.dispose(); } } diff --git a/src/vs/platform/storage/electron-main/storageIpc.ts b/src/vs/platform/storage/electron-main/storageIpc.ts index 87c392c46fd..9802b767d91 100644 --- a/src/vs/platform/storage/electron-main/storageIpc.ts +++ b/src/vs/platform/storage/electron-main/storageIpc.ts @@ -105,18 +105,6 @@ export class StorageDatabaseChannel extends Disposable implements IServerChannel break; } - case 'close': { - - // We only allow to close workspace scoped storage because - // global storage is shared across all windows and closes - // only on shutdown. - if (workspace) { - return storage.close(); - } - - break; - } - default: throw new Error(`Call not found: ${command}`); } diff --git a/src/vs/platform/storage/electron-main/storageMain.ts b/src/vs/platform/storage/electron-main/storageMain.ts index fbc5744c7df..384deb88135 100644 --- a/src/vs/platform/storage/electron-main/storageMain.ts +++ b/src/vs/platform/storage/electron-main/storageMain.ts @@ -212,43 +212,51 @@ abstract class BaseStorageMain extends Disposable implements IStorageMain { // a chance that the underlying DB is large // either on disk or in general. In that case // log some additional info to further diagnose - if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD && this.path) { - try { - const largestEntries = top(Array.from(this._storage.items.entries()) - .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) - .map(entry => `${entry.key}:${entry.length}`).join(', '); - const dbSize = (await this.fileService.stat(URI.file(this.path))).size; - - this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); - - type StorageSlowCloseClassification = { - duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The time it took to close the DB in ms.' }; - size: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The size of the DB in bytes.' }; - largestEntries: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The 5 largest keys in the DB.' }; - owner: 'bpasero'; - comment: 'Used to gain insight into reasons a database may be slow. This is used to assist with further optimizations'; - }; - - type StorageSlowCloseEvent = { - duration: number; - size: number; - largestEntries: string; - }; - - this.telemetryService.publicLog2('storageSlowClose', { - duration: watch.elapsed(), - size: dbSize, - largestEntries - }); - } catch (error) { - this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); - } + if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { + await this.logSlowClose(watch); } // Signal as event this._onDidCloseStorage.fire(); } + private async logSlowClose(watch: StopWatch) { + if (!this.path) { + return; + } + + try { + const largestEntries = top(Array.from(this._storage.items.entries()) + .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) + .map(entry => `${entry.key}:${entry.length}`).join(', '); + const dbSize = (await this.fileService.stat(URI.file(this.path))).size; + + this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); + + type StorageSlowCloseClassification = { + duration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The time it took to close the DB in ms.' }; + size: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The size of the DB in bytes.' }; + largestEntries: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'The 5 largest keys in the DB.' }; + owner: 'bpasero'; + comment: 'Used to gain insight into reasons a database may be slow. This is used to assist with further optimizations'; + }; + + type StorageSlowCloseEvent = { + duration: number; + size: number; + largestEntries: string; + }; + + this.telemetryService.publicLog2('storageSlowClose', { + duration: watch.elapsed(), + size: dbSize, + largestEntries + }); + } catch (error) { + this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); + } + } + private async doClose(): Promise { // Ensure we are not accidentally leaving diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 6781158c321..aee6d47d758 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -76,7 +76,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic })(); // Workspace Storage: Warmup when related window with workspace loads - this._register(this.lifecycleMainService.onWillLoadWindow(async e => { + this._register(this.lifecycleMainService.onWillLoadWindow(e => { if (e.workspace) { this.workspaceStorage(e.workspace).init(); } @@ -142,9 +142,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic private createWorkspaceStorage(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IEmptyWorkspaceIdentifier): IStorageMain { if (this.shutdownReason === ShutdownReason.KILL) { + // Workaround for native crashes that we see when // SQLite DBs are being created even after shutdown // https://github.com/microsoft/vscode/issues/143186 + return new InMemoryStorageMain(this.logService, this.fileService, this.telemetryService); } diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index 48dfb1cc8fe..74d12ea8086 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -15,22 +15,14 @@ export interface IServerTelemetryService extends ITelemetryService { updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise; } -interface CachedTelemetryEvent { - eventName: string; - data?: ITelemetryData; - anonymizeFilePaths?: boolean; - eventType: 'usage' | 'error'; -} - export class ServerTelemetryService extends TelemetryService implements IServerTelemetryService { - private _telemetryCache: CachedTelemetryEvent[] = []; // Because we cannot read the workspace config on the remote site // the ServerTelemetryService is responsible for knowing its telemetry level // this is done through IPC calls and initial value injections - private _injectedTelemetryLevel: TelemetryLevel | undefined; + private _injectedTelemetryLevel: TelemetryLevel; constructor( config: ITelemetryServiceConfig, - injectedTelemetryLevel: TelemetryLevel | undefined, + injectedTelemetryLevel: TelemetryLevel, @IConfigurationService _configurationService: IConfigurationService, @IProductService _productService: IProductService ) { @@ -39,11 +31,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT } override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - if (this._injectedTelemetryLevel === undefined) { - // Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor - this._telemetryCache?.push({ eventName, data, anonymizeFilePaths, eventType: 'usage' }); - return Promise.resolve(); - } if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) { return Promise.resolve(undefined); } @@ -55,11 +42,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT } override publicLogError(errorEventName: string, data?: ITelemetryData): Promise { - if (this._injectedTelemetryLevel === undefined) { - // Undefined safety with cache in case super class calls log before cache is initialized in subclass constructor - this._telemetryCache?.push({ eventName: errorEventName, data, eventType: 'error' }); - return Promise.resolve(); - } if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) { return Promise.resolve(undefined); } @@ -70,21 +52,6 @@ export class ServerTelemetryService extends TelemetryService implements IServerT return this.publicLogError(eventName, data as ITelemetryData | undefined); } - // Flushes all the cached events with the new level - async flushTelemetryCache(): Promise { - if (this._telemetryCache?.length === 0) { - return; - } - for (const cacheItem of this._telemetryCache) { - if (cacheItem.eventType === 'usage') { - await this.publicLog(cacheItem.eventName, cacheItem.data, cacheItem.anonymizeFilePaths); - } else { - await this.publicLogError(cacheItem.eventName, cacheItem.data); - } - } - this._telemetryCache = []; - } - async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise { if (telemetryLevel === undefined) { this._injectedTelemetryLevel = TelemetryLevel.NONE; @@ -93,11 +60,7 @@ export class ServerTelemetryService extends TelemetryService implements IServerT // We always take the most restrictive level because we don't want multiple clients to connect and send data when one client does not consent this._injectedTelemetryLevel = this._injectedTelemetryLevel ? Math.min(this._injectedTelemetryLevel, telemetryLevel) : telemetryLevel; if (this._injectedTelemetryLevel === TelemetryLevel.NONE) { - this._telemetryCache = []; this.dispose(); - } else { - // Level was set we're no longer in a pending state we flush the telemetry cache. - return this.flushTelemetryCache(); } } } diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index be381cd790c..1577949b1e2 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { IObservableValue } from 'vs/base/common/observableValue'; export const ITelemetryService = createDecorator('telemetryService'); @@ -46,7 +47,7 @@ export interface ITelemetryService { setExperimentProperty(name: string, value: string): void; - telemetryLevel: TelemetryLevel; + readonly telemetryLevel: IObservableValue; } export interface ITelemetryEndpoint { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index f16e3f0d240..e6ad0d44982 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -5,6 +5,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { cloneAndChange, mixin } from 'vs/base/common/objects'; +import { MutableObservableValue } from 'vs/base/common/observableValue'; import { isWeb } from 'vs/base/common/platform'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; @@ -35,9 +36,10 @@ export class TelemetryService implements ITelemetryService { private _commonProperties: Promise<{ [name: string]: any }>; private _experimentProperties: { [name: string]: string } = {}; private _piiPaths: string[]; - private _telemetryLevel: TelemetryLevel; private _sendErrorTelemetry: boolean; + public readonly telemetryLevel = new MutableObservableValue(TelemetryLevel.USAGE); + private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; @@ -49,7 +51,6 @@ export class TelemetryService implements ITelemetryService { this._appenders = config.appenders; this._commonProperties = config.commonProperties || Promise.resolve({}); this._piiPaths = config.piiPaths || []; - this._telemetryLevel = TelemetryLevel.USAGE; this._sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` @@ -59,7 +60,6 @@ export class TelemetryService implements ITelemetryService { this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); } - this._updateTelemetryLevel(); this._configurationService.onDidChangeConfiguration(this._updateTelemetryLevel, this, this._disposables); } @@ -69,19 +69,17 @@ export class TelemetryService implements ITelemetryService { } private _updateTelemetryLevel(): void { - this._telemetryLevel = getTelemetryLevel(this._configurationService); + let level = getTelemetryLevel(this._configurationService); const collectableTelemetry = this._productService.enabledTelemetryLevels; // Also ensure that error telemetry is respecting the product configuration for collectable telemetry if (collectableTelemetry) { this._sendErrorTelemetry = this.sendErrorTelemetry ? collectableTelemetry.error : false; // Make sure the telemetry level from the service is the minimum of the config and product const maxCollectableTelemetryLevel = collectableTelemetry.usage ? TelemetryLevel.USAGE : collectableTelemetry.error ? TelemetryLevel.ERROR : TelemetryLevel.NONE; - this._telemetryLevel = Math.min(this._telemetryLevel, maxCollectableTelemetryLevel); + level = Math.min(level, maxCollectableTelemetryLevel); } - } - get telemetryLevel(): TelemetryLevel { - return this._telemetryLevel; + this.telemetryLevel.value = level; } get sendErrorTelemetry(): boolean { @@ -106,7 +104,7 @@ export class TelemetryService implements ITelemetryService { private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { // don't send events when the user is optout - if (this.telemetryLevel < eventLevel) { + if (this.telemetryLevel.value < eventLevel) { return Promise.resolve(undefined); } diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index d74b997e1dd..45757fec68b 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -5,6 +5,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { safeStringify } from 'vs/base/common/objects'; +import { staticObservableValue } from 'vs/base/common/observableValue'; import { isObject } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ConfigurationTarget, ConfigurationTargetToString, IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -31,7 +32,7 @@ export class NullTelemetryServiceShape implements ITelemetryService { } setExperimentProperty() { } - telemetryLevel = TelemetryLevel.NONE; + telemetryLevel = staticObservableValue(TelemetryLevel.NONE); getTelemetryInfo(): Promise { return Promise.resolve({ instanceId: 'someValue.instanceId', diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 9fbe21f1057..808db0e4a82 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -800,15 +800,15 @@ suite('TelemetryService', () => { } }(), TestProductService); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.NONE); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.NONE); telemetryLevel = TelemetryConfiguration.ON; emitter.fire({}); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.USAGE); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.USAGE); telemetryLevel = TelemetryConfiguration.ERROR; emitter.fire({}); - assert.strictEqual(service.telemetryLevel, TelemetryLevel.ERROR); + assert.strictEqual(service.telemetryLevel.value, TelemetryLevel.ERROR); service.dispose(); }); diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index dcbff469770..a679186010f 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -12,6 +12,7 @@ import * as process from 'vs/base/common/process'; import { format } from 'vs/base/common/strings'; import { isString } from 'vs/base/common/types'; import * as pfs from 'vs/base/node/pfs'; +import { ILogService } from 'vs/platform/log/common/log'; import { IShellLaunchConfig, ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; export function getWindowsBuildNumber(): number { @@ -102,13 +103,14 @@ export interface IShellIntegrationConfigInjection { */ export function getShellIntegrationInjection( shellLaunchConfig: IShellLaunchConfig, - options: ITerminalProcessOptions['shellIntegration'] + options: ITerminalProcessOptions['shellIntegration'], + logService: ILogService ): IShellIntegrationConfigInjection | undefined { // Shell integration arg injection is disabled when: // - The global setting is disabled // - There is no executable (not sure what script to run) // - The terminal is used by a feature like tasks or debugging - if (!options.enabled || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal) { + if (!options.enabled || !shellLaunchConfig.executable || shellLaunchConfig.isFeatureTerminal || shellLaunchConfig.hideFromUser) { return undefined; } @@ -135,6 +137,7 @@ export function getShellIntegrationInjection( } return { newArgs }; } + logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return undefined; } @@ -193,13 +196,21 @@ export function getShellIntegrationInjection( source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh'), dest: path.join(zdotdir, '.zshrc') }); + filesToCopy.push({ + source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh'), + dest: path.join(zdotdir, '.zprofile') + }); + filesToCopy.push({ + source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh'), + dest: path.join(zdotdir, '.zshenv') + }); if (!options.showWelcome) { envMixin['VSCODE_SHELL_HIDE_WELCOME'] = '1'; } return { newArgs, envMixin, filesToCopy }; } } - + logService.warn(`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`, shellLaunchConfig.args); return undefined; } diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index a7c1d444d4e..4b4be142397 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -198,11 +198,9 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } let injection: IShellIntegrationConfigInjection | undefined; - if (this._options.shellIntegration) { - injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options.shellIntegration); - if (!injection) { - this._logService.warn(`Shell integration cannot be enabled for executable "${this.shellLaunchConfig.executable}" and args`, this.shellLaunchConfig.args); - } else { + if (this._options.shellIntegration.enabled) { + injection = getShellIntegrationInjection(this.shellLaunchConfig, this._options.shellIntegration, this._logService); + if (injection) { if (injection.envMixin) { for (const [key, value] of Object.entries(injection.envMixin)) { this._ptyOptions.env ||= {}; diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 84fb8a7e08d..8dffe31a645 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual, ok, strictEqual } from 'assert'; +import { NullLogService } from 'vs/platform/log/common/log'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { getShellIntegrationInjection, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -11,13 +12,14 @@ const enabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { ena const disabledProcessOptions: ITerminalProcessOptions['shellIntegration'] = { enabled: false, showWelcome: true }; const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); +const logService = new NullLogService(); suite('platform - terminalEnvironment', () => { suite('getShellIntegrationInjection', () => { suite('should not enable', () => { test('when isFeatureTerminal or when no executable is provided', () => { - ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions)); - ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions)); + ok(!getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: true }, enabledProcessOptions, logService)); + ok(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'], isFeatureTerminal: false }, enabledProcessOptions, logService)); }); }); @@ -34,21 +36,21 @@ suite('platform - terminalEnvironment', () => { ] }); test('when undefined, []', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions, logService), enabledExpectedResult); }); suite('when no logo', () => { test('array - case insensitive', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-nol'] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOL'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOLOGO'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-nol'] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-NOL'] }, enabledProcessOptions, logService), enabledExpectedResult); }); test('string - case insensitive', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NoLogo' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOLOGO' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-nol' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOL' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NoLogo' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOLOGO' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-nol' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-NOL' }, enabledProcessOptions, logService), enabledExpectedResult); }); }); }); @@ -62,23 +64,23 @@ suite('platform - terminalEnvironment', () => { ] }); test('when array contains no logo and login', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo'] }, enabledProcessOptions, logService), enabledExpectedResult); }); test('when string', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, enabledProcessOptions, logService), enabledExpectedResult); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-l' }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: undefined }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: ['-l', '-NoLogo', '-i'] }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg (string)', () => { - strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: pwshExe, args: '-i' }, disabledProcessOptions, logService), undefined); }); }); }); @@ -87,37 +89,45 @@ suite('platform - terminalEnvironment', () => { suite('zsh', () => { suite('should override args', () => { const expectedDir = /.+\/vscode-zsh/; - const expectedDest = /.+\/vscode-zsh\/.zshrc/; - const expectedSource = /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration.zsh/; + const expectedDests = [/.+\/vscode-zsh\/.zshrc/, /.+\/vscode-zsh\/.zprofile/, /.+\/vscode-zsh\/.zshenv/]; + const expectedSources = [ + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration.zsh/, + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration-profile.zsh/, + /.+\/out\/vs\/workbench\/contrib\/terminal\/browser\/media\/shellIntegration-env.zsh/ + ]; function assertIsEnabled(result: IShellIntegrationConfigInjection) { strictEqual(Object.keys(result.envMixin!).length, 1); ok(result.envMixin!['ZDOTDIR']?.match(expectedDir)); - strictEqual(result.filesToCopy?.length, 1); - ok(result.filesToCopy[0].dest.match(expectedDest)); - ok(result.filesToCopy[0].source.match(expectedSource)); + strictEqual(result.filesToCopy?.length, 3); + ok(result.filesToCopy[0].dest.match(expectedDests[0])); + ok(result.filesToCopy[1].dest.match(expectedDests[1])); + ok(result.filesToCopy[2].dest.match(expectedDests[2])); + ok(result.filesToCopy[0].source.match(expectedSources[0])); + ok(result.filesToCopy[1].source.match(expectedSources[1])); + ok(result.filesToCopy[2].source.match(expectedSources[2])); } test('when undefined, []', () => { - const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions); + const result1 = getShellIntegrationInjection({ executable: 'zsh', args: [] }, enabledProcessOptions, logService); deepStrictEqual(result1?.newArgs, ['-i']); assertIsEnabled(result1); - const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions); + const result2 = getShellIntegrationInjection({ executable: 'zsh', args: undefined }, enabledProcessOptions, logService); deepStrictEqual(result2?.newArgs, ['-i']); assertIsEnabled(result2); }); suite('should incorporate login arg', () => { test('when array', () => { - const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions); + const result = getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, enabledProcessOptions, logService); deepStrictEqual(result?.newArgs, ['-il']); assertIsEnabled(result); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: undefined }, disabledProcessOptions, logService), undefined); }); test('when using unrecognized arg', () => { - strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'zsh', args: ['-l', '-fake'] }, disabledProcessOptions, logService), undefined); }); }); }); @@ -132,9 +142,9 @@ suite('platform - terminalEnvironment', () => { ], envMixin: {} }); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions), enabledExpectedResult); - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: [] }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: '' }, enabledProcessOptions, logService), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, enabledProcessOptions, logService), enabledExpectedResult); }); suite('should set login env variable and not modify args', () => { const enabledExpectedResult = Object.freeze({ @@ -147,16 +157,16 @@ suite('platform - terminalEnvironment', () => { } }); test('when array', () => { - deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions), enabledExpectedResult); + deepStrictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, enabledProcessOptions, logService), enabledExpectedResult); }); }); suite('should not modify args', () => { test('when shell integration is disabled', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions), undefined); - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l'] }, disabledProcessOptions, logService), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: undefined }, disabledProcessOptions, logService), undefined); }); test('when custom array entry', () => { - strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions), undefined); + strictEqual(getShellIntegrationInjection({ executable: 'bash', args: ['-l', '-i'] }, disabledProcessOptions, logService), undefined); }); }); }); diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 5adf20c74e4..86737bd1657 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -57,7 +57,7 @@ export abstract class AbstractUpdateService implements IUpdateService { * optimization, to avoid using extra CPU cycles before first window open. * https://github.com/microsoft/vscode/issues/89784 */ - initialize(): void { + async initialize(): Promise { if (!this.environmentMainService.isBuilt) { return; // updates are never enabled when running out of sources } @@ -72,7 +72,7 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - const updateMode = this.getUpdateMode(); + const updateMode = await this.getUpdateMode(); const quality = this.getProductQuality(updateMode); if (!quality) { @@ -104,7 +104,7 @@ export abstract class AbstractUpdateService implements IUpdateService { } } - private getUpdateMode(): 'none' | 'manual' | 'start' | 'default' { + protected async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { return getMigratedSettingValue<'none' | 'manual' | 'start' | 'default'>(this.configurationService, 'update.mode', 'update.channel'); } @@ -184,7 +184,11 @@ export abstract class AbstractUpdateService implements IUpdateService { async isLatestVersion(): Promise { if (!this.url) { return undefined; - } else if (this.getUpdateMode() === 'none') { + } + + const mode = await this.getUpdateMode(); + + if (mode === 'none') { return false; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 0c008ea8504..688ea356ef1 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -38,8 +38,8 @@ export class DarwinUpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - override initialize(): void { - super.initialize(); + override async initialize(): Promise { + await super.initialize(); this.onRawError(this.onError, this, this.disposables); this.onRawUpdateAvailable(this.onUpdateAvailable, this, this.disposables); this.onRawUpdateDownloaded(this.onUpdateDownloaded, this, this.disposables); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 781e766b7f7..e3661ce4bbc 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -47,6 +47,14 @@ function getUpdateType(): UpdateType { return _updateType; } +function validateUpdateModeValue(value: string | undefined): 'none' | 'manual' | 'start' | 'default' | undefined { + if (value === 'none' || value === 'manual' || value === 'start' || value === 'default') { + return value; + } else { + return undefined; + } +} + export class Win32UpdateService extends AbstractUpdateService { private availableUpdate: IAvailableUpdate | undefined; @@ -71,8 +79,24 @@ export class Win32UpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - override initialize(): void { - super.initialize(); + protected override async getUpdateMode(): Promise<'none' | 'manual' | 'start' | 'default'> { + if (this.productService.win32RegValueName) { + const policyKey = `Software\\Policies\\Microsoft\\${this.productService.win32RegValueName}`; + const [hklm, hkcu] = await Promise.all([ + this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_LOCAL_MACHINE', policyKey, 'UpdateMode').then(validateUpdateModeValue), + this.nativeHostMainService.windowsGetStringRegKey(undefined, 'HKEY_CURRENT_USER', policyKey, 'UpdateMode').then(validateUpdateModeValue) + ]); + + if (hklm) { + this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKLM\\${policyKey}':`, hklm); + return hklm; + } else if (hkcu) { + this.logService.info(`update#getUpdateMode: 'UpdateMode' policy defined in 'HKCU\\${policyKey}':`, hkcu); + return hkcu; + } + } + + return await super.getUpdateMode(); } protected buildUpdateFeedUrl(quality: string): string | undefined { diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 9fd60d23a45..2c9591a2bec 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -8,6 +8,7 @@ import { isLinux, isMacintosh, isNative, isWeb } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ISandboxConfiguration } from 'vs/base/parts/sandbox/common/sandboxTypes'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { LogLevel } from 'vs/platform/log/common/log'; @@ -167,43 +168,44 @@ export function getTitleBarStyle(configurationService: IConfigurationService): ' return isLinux ? 'native' : 'custom'; // default to custom on all macOS and Windows } -export interface IPath extends IPathData { +export interface IPath extends IPathData { - // the file path to open within the instance + /** + * The file path to open within the instance + */ fileUri?: URI; } -export interface IPathData { +export interface IPathData { - // the file path to open within the instance + /** + * The file path to open within the instance + */ readonly fileUri?: UriComponents; /** - * An optional selection to apply in the file + * Optional editor options to apply in the file */ - readonly selection?: { - readonly startLineNumber: number; - readonly startColumn: number; - readonly endLineNumber?: number; - readonly endColumn?: number; - }; + readonly options?: T; - // a hint that the file exists. if true, the - // file exists, if false it does not. with - // `undefined` the state is unknown. + /** + * A hint that the file exists. if true, the + * file exists, if false it does not. with + * `undefined` the state is unknown. + */ readonly exists?: boolean; - // a hint about the file type of this path. - // with `undefined` the type is unknown. + /** + * A hint about the file type of this path. + * with `undefined` the type is unknown. + */ readonly type?: FileType; - // Specifies if the file should be only be opened - // if it exists + /** + * Specifies if the file should be only be opened + * if it exists. + */ readonly openOnlyIfExists?: boolean; - - // Specifies an optional id to override the editor - // used to edit the resource, e.g. custom editor. - readonly editorOverrideId?: string; } export interface IPathsToWaitFor extends IPathsToWaitForData { diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 77171dabff1..cc6749e78e4 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -29,7 +29,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { IProtocolMainService } from 'vs/platform/protocol/electron-main/protocol'; import { resolveMarketplaceHeaders } from 'vs/platform/externalServices/common/marketplace'; -import { IGlobalStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; +import { IGlobalStorageMainService, IStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; @@ -106,23 +106,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { private _lastFocusTime = -1; get lastFocusTime(): number { return this._lastFocusTime; } - get backupPath(): string | undefined { return this.currentConfig?.backupPath; } + get backupPath(): string | undefined { return this._config?.backupPath; } - get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this.currentConfig?.workspace; } + get openedWorkspace(): IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined { return this._config?.workspace; } - get remoteAuthority(): string | undefined { return this.currentConfig?.remoteAuthority; } + get remoteAuthority(): string | undefined { return this._config?.remoteAuthority; } - private currentConfig: INativeWindowConfiguration | undefined; - get config(): INativeWindowConfiguration | undefined { return this.currentConfig; } + private _config: INativeWindowConfiguration | undefined; + get config(): INativeWindowConfiguration | undefined { return this._config; } private hiddenTitleBarStyle: boolean | undefined; get hasHiddenTitleBarStyle(): boolean { return !!this.hiddenTitleBarStyle; } - get isExtensionDevelopmentHost(): boolean { return !!(this.currentConfig?.extensionDevelopmentPath); } + get isExtensionDevelopmentHost(): boolean { return !!(this._config?.extensionDevelopmentPath); } - get isExtensionTestHost(): boolean { return !!(this.currentConfig?.extensionTestsPath); } + get isExtensionTestHost(): boolean { return !!(this._config?.extensionTestsPath); } - get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this.currentConfig?.debugId; } + get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this._config?.debugId; } //#endregion @@ -150,6 +150,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IFileService private readonly fileService: IFileService, @IGlobalStorageMainService private readonly globalStorageMainService: IGlobalStorageMainService, + @IStorageMainService private readonly storageMainService: IStorageMainService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeMainService private readonly themeMainService: IThemeMainService, @IWorkspacesManagementMainService private readonly workspacesManagementMainService: IWorkspacesManagementMainService, @@ -439,7 +440,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Associate properties from the load request if provided if (this.pendingLoadConfig) { - this.currentConfig = this.pendingLoadConfig; + this._config = this.pendingLoadConfig; this.pendingLoadConfig = undefined; } @@ -452,16 +453,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Window (Un)Maximize this._win.on('maximize', (e: Event) => { - if (this.currentConfig) { - this.currentConfig.maximized = true; + if (this._config) { + this._config.maximized = true; } app.emit('browser-window-maximize', e, this._win); }); this._win.on('unmaximize', (e: Event) => { - if (this.currentConfig) { - this.currentConfig.maximized = false; + if (this._config) { + this._config.maximized = false; } app.emit('browser-window-unmaximize', e, this._win); @@ -496,6 +497,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (!this.marketplaceHeadersPromise) { this.marketplaceHeadersPromise = resolveMarketplaceHeaders(this.productService.version, this.productService, this.environmentMainService, this.configurationService, this.fileService, this.globalStorageMainService); } + return this.marketplaceHeadersPromise; } @@ -545,8 +547,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // If we run smoke tests, we never want to show a blocking dialog - if (this.environmentMainService.driverHandle) { - this.destroyWindow(false); + if (this.environmentMainService.args['enable-smoke-test-driver']) { + this.destroyWindow(false, false); return; } @@ -575,13 +577,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { detail: localize('appStalledDetail', "You can reopen or close the window or keep waiting."), noLink: true, defaultId: 0, - cancelId: 1 + cancelId: 1, + checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined }, this._win); // Handle choice if (result.response !== 1 /* keep waiting */) { const reopen = result.response === 0; - this.destroyWindow(reopen); + this.destroyWindow(reopen, result.checkboxChecked); } } @@ -605,18 +608,32 @@ export class CodeWindow extends Disposable implements ICodeWindow { message, detail: localize('appCrashedDetail', "We are sorry for the inconvenience. You can reopen the window to continue where you left off."), noLink: true, - defaultId: 0 + defaultId: 0, + checkboxLabel: this._config?.workspace ? localize('doNotRestoreEditors', "Don't restore editors") : undefined }, this._win); // Handle choice const reopen = result.response === 0; - this.destroyWindow(reopen); + this.destroyWindow(reopen, result.checkboxChecked); } break; } } - private destroyWindow(reopen: boolean): void { + private async destroyWindow(reopen: boolean, skipRestoreEditors: boolean): Promise { + const workspace = this._config?.workspace; + + // check to discard editor state first + if (skipRestoreEditors && workspace) { + try { + const workspaceStorage = this.storageMainService.workspaceStorage(workspace); + await workspaceStorage.init(); + workspaceStorage.delete('memento/workbench.parts.editor'); + await workspaceStorage.close(); + } catch (error) { + this.logService.error(error); + } + } // 'close' event will not be fired on destroy(), so signal crash via explicit event this._onDidDestroy.fire(); @@ -625,15 +642,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._win?.destroy(); // ask the windows service to open a new fresh window if specified - if (reopen && this.config) { + if (reopen && this._config) { // We have to reconstruct a openable from the current workspace - let workspace: IWorkspaceToOpen | IFolderToOpen | undefined = undefined; + let uriToOpen: IWorkspaceToOpen | IFolderToOpen | undefined = undefined; let forceEmpty = undefined; - if (isSingleFolderWorkspaceIdentifier(this.openedWorkspace)) { - workspace = { folderUri: this.openedWorkspace.uri }; - } else if (isWorkspaceIdentifier(this.openedWorkspace)) { - workspace = { workspaceUri: this.openedWorkspace.configPath }; + if (isSingleFolderWorkspaceIdentifier(workspace)) { + uriToOpen = { folderUri: workspace.uri }; + } else if (isWorkspaceIdentifier(workspace)) { + uriToOpen = { workspaceUri: workspace.configPath }; } else { forceEmpty = true; } @@ -641,12 +658,12 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Delegate to windows service const [window] = this.windowsMainService.open({ context: OpenContext.API, - userEnv: this.config.userEnv, + userEnv: this._config.userEnv, cli: { ...this.environmentMainService.args, _: [] // we pass in the workspace to open explicitly via `urisToOpen` }, - urisToOpen: workspace ? [workspace] : undefined, + urisToOpen: uriToOpen ? [uriToOpen] : undefined, forceEmpty, forceNewWindow: true, remoteAuthority: this.remoteAuthority @@ -659,8 +676,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Make sure to update our workspace config if we detect that it // was deleted - if (this.openedWorkspace?.id === workspace.id && this.currentConfig) { - this.currentConfig.workspace = undefined; + if (this._config?.workspace?.id === workspace.id && this._config) { + this._config.workspace = undefined; } } @@ -726,7 +743,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work if (this.readyState === ReadyState.NONE) { - this.currentConfig = configuration; + this._config = configuration; } // Otherwise, the window is currently showing a folder and if there is an @@ -777,7 +794,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Also, preserve the environment if we're loading from an // extension development host that had its environment set // (for https://github.com/microsoft/vscode/issues/123508) - const currentUserEnv = (this.currentConfig ?? this.pendingLoadConfig)?.userEnv; + const currentUserEnv = (this._config ?? this.pendingLoadConfig)?.userEnv; if (currentUserEnv) { const shouldPreserveLaunchCliEnvironment = isLaunchedFromCli(currentUserEnv) && !isLaunchedFromCli(configuration.userEnv); const shouldPreserveDebugEnvironmnet = this.isExtensionDevelopmentHost; @@ -817,7 +834,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { async reload(cli?: NativeParsedArgs): Promise { // Copy our current config for reuse - const configuration = Object.assign({}, this.currentConfig); + const configuration = Object.assign({}, this._config); // Validate workspace configuration.workspace = await this.validateWorkspaceBeforeReload(configuration); diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 1dbfaaf9df9..77294070103 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -50,6 +50,7 @@ import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-m import { IWorkspacesManagementMainService } from 'vs/platform/workspaces/electron-main/workspacesManagementMainService'; import { ICodeWindow, UnloadReason } from 'vs/platform/window/electron-main/window'; import { IThemeMainService } from 'vs/platform/theme/electron-main/themeMainService'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; //#region Helper Interfaces @@ -118,23 +119,33 @@ interface IFilesToOpen { filesToWait?: IPathsToWaitFor; } -interface IPathToOpen extends IPath { +interface IPathToOpen extends IPath { - // the workspace to open + /** + * The workspace to open + */ readonly workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier; - // whether the path is considered to be transient or not - // for example, a transient workspace should not add to - // the workspaces history and should never restore + /** + * Whether the path is considered to be transient or not + * for example, a transient workspace should not add to + * the workspaces history and should never restore. + */ readonly transient?: boolean; - // the backup path to use + /** + * The backup path to use + */ readonly backupPath?: string; - // the remote authority for the Code instance to open. Undefined if not remote. + /** + * The remote authority for the Code instance to open. Undefined if not remote. + */ readonly remoteAuthority?: string; - // optional label for the recent history + /** + * Optional label for the recent history + */ label?: string; } @@ -916,7 +927,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return this.doResolveRemoteOpenable(openable, options); } - private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolveRemoteOpenable(openable: IWindowOpenable, options: IPathResolveOptions): IPathToOpen | undefined { let uri = this.resourceFromOpenable(openable); // use remote authority from vscode @@ -932,7 +943,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return { fileUri: uri.with({ path }), - selection: line ? { startLineNumber: line, startColumn: column || 1 } : undefined, + options: { + selection: line ? { startLineNumber: line, startColumn: column || 1 } : undefined + }, remoteAuthority }; } @@ -961,7 +974,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return openable.fileUri; } - private doResolveFilePath(path: string, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolveFilePath(path: string, options: IPathResolveOptions): IPathToOpen | undefined { // Extract line/col information from path let lineNumber: number | undefined; @@ -1004,7 +1017,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic fileUri: URI.file(path), type: FileType.File, exists: true, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + } }; } @@ -1047,7 +1062,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return undefined; } - private doResolvePathRemote(path: string, options: IPathResolveOptions): IPathToOpen | undefined { + private doResolvePathRemote(path: string, options: IPathResolveOptions): IPathToOpen | undefined { const first = path.charCodeAt(0); const remoteAuthority = options.remoteAuthority; @@ -1081,7 +1096,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (options.forceOpenWorkspaceAsFile) { return { fileUri: uri, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined, + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + }, remoteAuthority: options.remoteAuthority }; } @@ -1093,7 +1110,9 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic else if (options.gotoLineMode || posix.basename(path).indexOf('.') !== -1) { return { fileUri: uri, - selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined, + options: { + selection: lineNumber ? { startLineNumber: lineNumber, startColumn: columnNumber || 1 } : undefined + }, remoteAuthority }; } diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 4915eb0c283..96a972433da 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -19,7 +19,7 @@ import { IExtHostReadyMessage, IExtHostSocketMessage, IExtHostReduceGraceTimeMes import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService'; import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { IExtensionHostStatusService } from 'vs/server/node/extensionHostStatusService'; export async function buildUserEnvironment(startParamsEnv: { [key: string]: string | null } = {}, withUserShellEnvironment: boolean, language: string, isDebug: boolean, environmentService: IServerEnvironmentService, logService: ILogService): Promise { diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index b94d231c5d5..f291461470d 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -26,7 +26,7 @@ export const serverOptions: OptionDescriptions = { 'print-ip-address': { type: 'boolean' }, 'accept-server-license-terms': { type: 'boolean', cat: 'o', description: nls.localize('acceptLicenseTerms', "If set, the user accepts the server license terms and the server will be started without a user prompt.") }, 'server-data-dir': { type: 'string', cat: 'o', description: nls.localize('serverDataDir', "Specifies the directory that server data is kept in.") }, - 'telemetry-level': { type: 'string', cat: 'o', args: 'level', description: nls.localize('telemetry-level', "Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'. If not specified, the server will await a connection before sending any telemetry. Setting this to 'off' is equivalent to --disable-telemetry") }, + 'telemetry-level': { type: 'string', cat: 'o', args: 'level', description: nls.localize('telemetry-level', "Sets the initial telemetry level. Valid levels are: 'off', 'crash', 'error' and 'all'. If not specified, the server will send telemetry until a client connects, it will then use the clients telemetry setting. Setting this to 'off' is equivalent to --disable-telemetry") }, /* ----- vs code options --- -- */ diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 8e14d6fdc2b..a072fa8bb0e 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -133,7 +133,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken piiPaths: getPiiPathsFromEnvironment(environmentService) }; const initialTelemetryLevelArg = environmentService.args['telemetry-level']; - let injectedTelemetryLevel: TelemetryLevel | undefined = undefined; + let injectedTelemetryLevel: TelemetryLevel = TelemetryLevel.USAGE; // Convert the passed in CLI argument into a telemetry level for the telemetry service if (initialTelemetryLevelArg === 'all') { injectedTelemetryLevel = TelemetryLevel.USAGE; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 37afee52e07..2fa247a959b 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -64,6 +64,7 @@ import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; import './mainThreadNotebookKernels'; +import './mainThreadNotebookProxyKernels'; import './mainThreadNotebookDocumentsAndEditors'; import './mainThreadNotebookRenderers'; import './mainThreadInteractive'; diff --git a/src/vs/workbench/api/browser/mainThreadExtensionService.ts b/src/vs/workbench/api/browser/mainThreadExtensionService.ts index 1b9316170ad..41309ab2bd9 100644 --- a/src/vs/workbench/api/browser/mainThreadExtensionService.ts +++ b/src/vs/workbench/api/browser/mainThreadExtensionService.ts @@ -26,6 +26,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { URI, UriComponents } from 'vs/base/common/uri'; import { FileAccess } from 'vs/base/common/network'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; @extHostNamedCustomer(MainContext.MainThreadExtensionService) export class MainThreadExtensionService implements MainThreadExtensionServiceShape { @@ -204,8 +205,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { const uriComponents = await this._actual.$getCanonicalURI(remoteAuthority, uri); return (uriComponents ? URI.revive(uriComponents) : uriComponents); } - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { - return this._actual.$startExtensionHost(enabledExtensionIds); + startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise { + return this._actual.$startExtensionHost(extensionsDelta); } extensionTestsExecute(): Promise { return this._actual.$extensionTestsExecute(); @@ -225,8 +226,8 @@ class ExtensionHostProxy implements IExtensionHostProxy { updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise { return this._actual.$updateRemoteConnectionData(connectionData); } - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { - return this._actual.$deltaExtensions(toAdd, toRemove); + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise { + return this._actual.$deltaExtensions(extensionsDelta); } test_latency(n: number): Promise { return this._actual.$test_latency(n); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 95716c771fb..3b5e9139ca3 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -487,7 +487,7 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread }; } - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void { const provider: languages.CompletionItemProvider = { triggerCharacters, _debugDisplayName: displayName, diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index b22dce389de..6b522da7c97 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -200,14 +200,14 @@ CommandsRegistry.registerCommand('_executeDataToNotebook', async (accessor, ...a } const dto = await info.serializer.dataToNotebook(bytes); - return NotebookDto.toNotebookDataDto(dto); + return new SerializableObjectWithBuffers(NotebookDto.toNotebookDataDto(dto)); }); CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...args) => { const [notebookType, dto] = args; assertType(typeof notebookType === 'string', 'string'); - assertType(typeof dto === 'object', 'NotebookDataDto'); + assertType(typeof dto === 'object'); const notebookService = accessor.get(INotebookService); const info = await notebookService.withNotebookDataProvider(notebookType); @@ -215,7 +215,7 @@ CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...a return; } - const data = NotebookDto.fromNotebookDataDto(dto); + const data = NotebookDto.fromNotebookDataDto(dto.value); const bytes = await info.serializer.notebookToData(data); return bytes; }); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 46febd36991..a8809c58016 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -15,11 +15,12 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IResolvedNotebookKernel, INotebookKernelChangeEvent, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; -abstract class MainThreadKernel implements INotebookKernel { +abstract class MainThreadKernel implements IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; private readonly _onDidChange = new Emitter(); private readonly preloads: { uri: URI; provides: string[] }[]; diff --git a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts new file mode 100644 index 00000000000..74e7290161d --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { INotebookKernelService, INotebookProxyKernel, INotebookProxyKernelChangeEvent, ProxyKernelState, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { ExtHostContext, ExtHostNotebookProxyKernelsShape, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from '../common/extHost.protocol'; +import { onUnexpectedError } from 'vs/base/common/errors'; + +abstract class MainThreadProxyKernel implements INotebookProxyKernel { + readonly type: NotebookKernelType.Proxy = NotebookKernelType.Proxy; + protected readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + readonly id: string; + readonly viewType: string; + readonly extension: ExtensionIdentifier; + readonly preloadProvides: string[] = []; + label: string; + description?: string; + detail?: string; + kind?: string; + supportedLanguages: string[] = []; + connectionState: ProxyKernelState; + + constructor(data: INotebookProxyKernelDto) { + this.id = data.id; + this.viewType = data.notebookType; + this.extension = data.extensionId; + + this.label = data.label; + this.description = data.description; + this.detail = data.detail; + this.kind = data.kind; + + this.connectionState = ProxyKernelState.Disconnected; + } + + update(data: Partial) { + const event: INotebookProxyKernelChangeEvent = Object.create(null); + if (data.label !== undefined) { + this.label = data.label; + event.label = true; + } + if (data.description !== undefined) { + this.description = data.description; + event.description = true; + } + if (data.detail !== undefined) { + this.detail = data.detail; + event.detail = true; + } + if (data.kind !== undefined) { + this.kind = data.kind; + event.kind = true; + } + + this._onDidChange.fire(event); + } + + abstract resolveKernel(): Promise; +} + +@extHostNamedCustomer(MainContext.MainThreadNotebookProxyKernels) +export class MainThreadNotebookProxyKernels implements MainThreadNotebookProxyKernelsShape { + + private readonly _disposables = new DisposableStore(); + + private readonly _proxyKernels = new Map(); + private readonly _proxyKernelProxy: ExtHostNotebookProxyKernelsShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + ) { + this._proxyKernelProxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebookProxyKernels); + } + + dispose(): void { + this._disposables.dispose(); + + for (let [, registration] of this._proxyKernels.values()) { + registration.dispose(); + } + } + + // -- Proxy kernel + + async $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise { + const that = this; + const proxyKernel = new class extends MainThreadProxyKernel { + async resolveKernel(): Promise { + try { + this.connectionState = ProxyKernelState.Initializing; + this._onDidChange.fire({ connectionState: true }); + const delegateKernel = await that._proxyKernelProxy.$resolveKernel(handle); + this.connectionState = ProxyKernelState.Connected; + this._onDidChange.fire({ connectionState: true }); + return delegateKernel; + } catch (err) { + onUnexpectedError(err); + this.connectionState = ProxyKernelState.Disconnected; + this._onDidChange.fire({ connectionState: true }); + return null; + } + } + }(data); + + const registration = this._notebookKernelService.registerKernel(proxyKernel); + this._proxyKernels.set(handle, [proxyKernel, registration]); + } + + $updateProxyKernel(handle: number, data: Partial): void { + const tuple = this._proxyKernels.get(handle); + if (tuple) { + tuple[0].update(data); + } + } + + $removeProxyKernel(handle: number): void { + const tuple = this._proxyKernels.get(handle); + if (tuple) { + tuple[1].dispose(); + this._proxyKernels.delete(handle); + } + } +} diff --git a/src/vs/workbench/api/browser/mainThreadOutputService.ts b/src/vs/workbench/api/browser/mainThreadOutputService.ts index 2c82719f4e8..34859bb4235 100644 --- a/src/vs/workbench/api/browser/mainThreadOutputService.ts +++ b/src/vs/workbench/api/browser/mainThreadOutputService.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { Extensions, IOutputChannelRegistry, IOutputService, IOutputChannel, OUTPUT_VIEW_ID, OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { MainThreadOutputServiceShape, MainContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { UriComponents, URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/api/browser/mainThreadSecretState.ts b/src/vs/workbench/api/browser/mainThreadSecretState.ts index 1a131d41518..539315d7c08 100644 --- a/src/vs/workbench/api/browser/mainThreadSecretState.ts +++ b/src/vs/workbench/api/browser/mainThreadSecretState.ts @@ -8,6 +8,7 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService'; import { ExtHostContext, ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol'; +import { ILogService } from 'vs/platform/log/common/log'; @extHostNamedCustomer(MainContext.MainThreadSecretState) export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape { @@ -19,6 +20,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre extHostContext: IExtHostContext, @ICredentialsService private readonly credentialsService: ICredentialsService, @IEncryptionService private readonly encryptionService: IEncryptionService, + @ILogService private readonly logService: ILogService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState); @@ -36,7 +38,26 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre async $getPassword(extensionId: string, key: string): Promise { const fullKey = await this.getFullKey(extensionId); const password = await this.credentialsService.getPassword(fullKey, key); - const decrypted = password && await this.encryptionService.decrypt(password); + if (!password) { + return undefined; + } + + let decrypted: string | null; + try { + decrypted = await this.encryptionService.decrypt(password); + } catch (e) { + this.logService.error(e); + + // If we are on a platform that newly started encrypting secrets before storing them, + // then passwords previously stored were stored un-encrypted (NOTE: but still being stored in a secure keyring). + // When we try to decrypt a password that wasn't encrypted previously, the encryption service will throw. + // To recover gracefully, we first try to encrypt & store the password (essentially migrating the secret to the new format) + // and then we try to read it and decrypt again. + const encryptedForSet = await this.encryptionService.encrypt(password); + await this.credentialsService.setPassword(fullKey, key, encryptedForSet); + const passwordEncrypted = await this.credentialsService.getPassword(fullKey, key); + decrypted = passwordEncrypted && await this.encryptionService.decrypt(passwordEncrypted); + } if (decrypted) { try { @@ -59,7 +80,7 @@ export class MainThreadSecretState extends Disposable implements MainThreadSecre content: value }); const encrypted = await this.encryptionService.encrypt(toEncrypt); - return this.credentialsService.setPassword(fullKey, key, encrypted); + return await this.credentialsService.setPassword(fullKey, key, encrypted); } async $deletePassword(extensionId: string, key: string): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadTelemetry.ts b/src/vs/workbench/api/browser/mainThreadTelemetry.ts index 6e13fa13053..ac89aca9280 100644 --- a/src/vs/workbench/api/browser/mainThreadTelemetry.ts +++ b/src/vs/workbench/api/browser/mainThreadTelemetry.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryService, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; -import { MainThreadTelemetryShape, MainContext, ExtHostTelemetryShape, ExtHostContext } from '../common/extHost.protocol'; -import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; -import { getTelemetryLevel, supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; +import { ITelemetryService, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ExtHostContext, ExtHostTelemetryShape, MainContext, MainThreadTelemetryShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadTelemetry) export class MainThreadTelemetry extends Disposable implements MainThreadTelemetryShape { @@ -22,7 +21,6 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet constructor( extHostContext: IExtHostContext, @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IConfigurationService private readonly _configurationService: IConfigurationService, @IEnvironmentService private readonly _environmentService: IEnvironmentService, @IProductService private readonly _productService: IProductService ) { @@ -31,10 +29,8 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTelemetry); if (supportsTelemetry(this._productService, this._environmentService)) { - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TELEMETRY_SETTING_ID) || e.affectsConfiguration(TELEMETRY_OLD_SETTING_ID)) { - this._proxy.$onDidChangeTelemetryLevel(this.telemetryLevel); - } + this._register(_telemetryService.telemetryLevel.onDidChange(level => { + this._proxy.$onDidChangeTelemetryLevel(level); })); } @@ -46,7 +42,7 @@ export class MainThreadTelemetry extends Disposable implements MainThreadTelemet return TelemetryLevel.NONE; } - return getTelemetryLevel(this._configurationService); + return this._telemetryService.telemetryLevel.value; } $publicLog(eventName: string, data: any = Object.create(null)): void { diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index 6636909bbdf..d6547f29752 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, InternalTimelineOptions, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, Timeline } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadTimeline) @@ -40,8 +40,8 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - return revive(await proxy.$getTimeline(provider.id, uri, options, token, internalOptions)); + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + return revive(await proxy.$getTimeline(provider.id, uri, options, token)); }, dispose() { emitters.delete(provider.id); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index acdf0672025..864b227de8e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -70,7 +70,6 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication'; import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; @@ -92,11 +91,17 @@ import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookE import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; import { combinedDisposable } from 'vs/base/common/lifecycle'; -import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug'; +import { ExtHostNotebookProxyKernels } from 'vs/workbench/api/common/extHostNotebookProxyKernels'; + +export interface IExtensionRegistries { + mine: ExtensionDescriptionRegistry; + all: ExtensionDescriptionRegistry; +} export interface IExtensionApiFactory { - (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; + (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode; } /** @@ -156,6 +161,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); + const extHostNotebookProxyKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookProxyKernels, new ExtHostNotebookProxyKernels(rpcProtocol, extHostNotebookKernels, extHostLogService)); const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); @@ -195,7 +201,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // Register API-ish commands ExtHostApiCommands.register(extHostCommands); - return function (extension: IExtensionDescription, extensionRegistry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode { + return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode { // Check document selectors for being overly generic. Technically this isn't a problem but // in practice many extensions say they support `fooLang` but need fs-access to do so. Those @@ -359,10 +365,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I Object.freeze(env); } - const extensionKind = initData.remote.isRemote - ? extHostTypes.ExtensionKind.Workspace - : extHostTypes.ExtensionKind.UI; - + // namespace: tests const tests: typeof vscode.tests = { createTestController(provider, label, refreshHandler?: (token: vscode.CancellationToken) => Thenable | void) { return extHostTesting.createTestController(provider, label, refreshHandler); @@ -386,19 +389,49 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }; // namespace: extensions + const extensionKind = initData.remote.isRemote + ? extHostTypes.ExtensionKind.Workspace + : extHostTypes.ExtensionKind.UI; + const extensions: typeof vscode.extensions = { - getExtension(extensionId: string): vscode.Extension | undefined { - const desc = extensionRegistry.getExtensionDescription(extensionId); - if (desc) { - return new Extension(extensionService, extension.identifier, desc, extensionKind); + getExtension(extensionId: string, includeFromDifferentExtensionHosts?: boolean): vscode.Extension | undefined { + if (!isProposedApiEnabled(extension, 'extensionsAny')) { + includeFromDifferentExtensionHosts = false; + } + const mine = extensionInfo.mine.getExtensionDescription(extensionId); + if (mine) { + return new Extension(extensionService, extension.identifier, mine, extensionKind, false); + } + if (includeFromDifferentExtensionHosts) { + const foreign = extensionInfo.all.getExtensionDescription(extensionId); + if (foreign) { + return new Extension(extensionService, extension.identifier, foreign, extensionKind /* TODO@alexdima THIS IS WRONG */, true); + } } return undefined; }, get all(): vscode.Extension[] { - return extensionRegistry.getAllExtensionDescriptions().map((desc) => new Extension(extensionService, extension.identifier, desc, extensionKind)); + const result: vscode.Extension[] = []; + for (const desc of extensionInfo.mine.getAllExtensionDescriptions()) { + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind, false)); + } + return result; + }, + get allAcrossExtensionHosts(): vscode.Extension[] { + checkProposedApiEnabled(extension, 'extensionsAny'); + const local = new ExtensionIdentifierSet(extensionInfo.mine.getAllExtensionDescriptions().map(desc => desc.identifier)); + const result: vscode.Extension[] = []; + for (const desc of extensionInfo.all.getAllExtensionDescriptions()) { + const isFromDifferentExtensionHost = !local.has(desc.identifier); + result.push(new Extension(extensionService, extension.identifier, desc, extensionKind /* TODO@alexdima THIS IS WRONG */, isFromDifferentExtensionHost)); + } + return result; }, get onDidChange() { - return extensionRegistry.onDidChange; + if (isProposedApiEnabled(extension, 'extensionsAny')) { + return Event.any(extensionInfo.mine.onDidChange, extensionInfo.all.onDidChange); + } + return extensionInfo.mine.onDidChange; } }; @@ -625,7 +658,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I showInputBox(options?: vscode.InputBoxOptions, token?: vscode.CancellationToken) { if (options?.validateInput2) { checkProposedApiEnabled(extension, 'inputBoxSeverity'); - options.validateInput = options.validateInput2 as any; } return extHostQuickOpen.showInput(options, token); }, @@ -758,7 +790,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata); }, get tabGroups(): vscode.TabGroups { - checkProposedApiEnabled(extension, 'tabs'); return extHostEditorTabs.tabGroups; }, getInlineCompletionItemController(provider: vscode.InlineCompletionItemProvider): vscode.InlineCompletionController { @@ -892,11 +923,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, onDidSaveNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); }, onDidChangeNotebookDocument(listener, thisArg, disposables) { - checkProposedApiEnabled(extension, 'notebookDocumentEvents'); return extHostNotebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); }, get onDidOpenNotebookDocument(): Event { @@ -1129,11 +1158,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, - createConcatTextDocument(notebook, selector) { - checkProposedApiEnabled(extension, 'notebookConcatTextDocument'); - extHostApiDeprecation.report('notebookConcatTextDocument', extension, 'This proposal is not on track for finalization and will be removed.'); - return new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook, selector); - }, + createNotebookProxyController(id: string, notebookType: string, label: string, handler: () => vscode.NotebookController | string | Thenable) { + checkProposedApiEnabled(extension, 'notebookProxyController'); + return extHostNotebookProxyKernels.createNotebookProxyController(extension, id, notebookType, label, handler); + } }; return { @@ -1315,13 +1343,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LanguageStatusSeverity: extHostTypes.LanguageStatusSeverity, QuickPickItemKind: extHostTypes.QuickPickItemKind, InputBoxValidationSeverity: extHostTypes.InputBoxValidationSeverity, - TabKindText: extHostTypes.TextTabInput, - TabKindTextDiff: extHostTypes.TextDiffTabInput, - TabKindCustom: extHostTypes.CustomEditorTabInput, - TabKindNotebook: extHostTypes.NotebookEditorTabInput, - TabKindNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, - TabKindWebview: extHostTypes.WebviewEditorTabInput, - TabKindTerminal: extHostTypes.TerminalEditorTabInput + TabInputText: extHostTypes.TextTabInput, + TabInputTextDiff: extHostTypes.TextDiffTabInput, + TabInputCustom: extHostTypes.CustomEditorTabInput, + TabInputNotebook: extHostTypes.NotebookEditorTabInput, + TabInputNotebookDiff: extHostTypes.NotebookDiffEditorTabInput, + TabInputWebview: extHostTypes.WebviewEditorTabInput, + TabInputTerminal: extHostTypes.TerminalEditorTabInput }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index b78a95454a6..4787ccb401b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -53,16 +53,16 @@ import * as notebookCommon from 'vs/workbench/contrib/notebook/common/notebookCo import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ISerializedTestResults, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, RunTestForControllerRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes'; -import { InternalTimelineOptions, Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; import { TypeHierarchyItem } from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { AuthenticationProviderInformation, AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/workbench/services/authentication/common/authentication'; import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; -import { IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IStaticWorkspaceData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; import { ActivationKind, ExtensionActivationReason, MissingExtensionDependency } from 'vs/workbench/services/extensions/common/extensions'; import { createProxyIdentifier, Dto, IRPCProtocol, SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -378,7 +378,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend, eventHandle: number | undefined): void; $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: languages.SemanticTokensLegend): void; - $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerCompletionsProvider(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerInlineCompletionsSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean, eventHandle: number | undefined, displayName: string | undefined): void; @@ -982,6 +982,17 @@ export interface INotebookKernelDto2 { preloads?: { uri: UriComponents; provides: string[] }[]; } +export interface INotebookProxyKernelDto { + id: string; + notebookType: string; + extensionId: ExtensionIdentifier; + extensionLocation: UriComponents; + label: string; + detail?: string; + description?: string; + kind?: string; +} + export interface ICellExecuteOutputEditDto { editType: CellExecutionUpdateType.Output; append?: boolean; @@ -1015,6 +1026,12 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $completeExecution(handle: number, data: SerializableObjectWithBuffers): void; } +export interface MainThreadNotebookProxyKernelsShape extends IDisposable { + $addProxyKernel(handle: number, data: INotebookProxyKernelDto): Promise; + $updateProxyKernel(handle: number, data: Partial): void; + $removeProxyKernel(handle: number): void; +} + export interface MainThreadNotebookRenderersShape extends IDisposable { $postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise; } @@ -1417,7 +1434,7 @@ export interface ExtHostExtensionServiceShape { * Returns `null` if no resolver for `remoteAuthority` is found. */ $getCanonicalURI(remoteAuthority: string, uri: UriComponents): Promise; - $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; + $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise; $extensionTestsExecute(): Promise; $extensionTestsExit(code: number): Promise; $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; @@ -1425,7 +1442,7 @@ export interface ExtHostExtensionServiceShape { $setRemoteEnvironment(env: { [key: string]: string | null }): Promise; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise; - $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; + $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise; $test_latency(n: number): Promise; $test_up(b: VSBuffer): Promise; @@ -2103,6 +2120,10 @@ export interface ExtHostNotebookKernelsShape { $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void; } +export interface ExtHostNotebookProxyKernelsShape { + $resolveKernel(handle: number): Promise; +} + export interface ExtHostInteractiveShape { $willAddInteractiveDocument(uri: UriComponents, eol: string, languageId: string, notebookUri: UriComponents): void; $willRemoveInteractiveDocument(uri: UriComponents, notebookUri: UriComponents): void; @@ -2138,7 +2159,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise | undefined>; + $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken): Promise | undefined>; } export const enum ExtHostTestingResource { @@ -2279,6 +2300,7 @@ export const MainContext = { MainThreadNotebookDocuments: createProxyIdentifier('MainThreadNotebookDocumentsShape'), MainThreadNotebookEditors: createProxyIdentifier('MainThreadNotebookEditorsShape'), MainThreadNotebookKernels: createProxyIdentifier('MainThreadNotebookKernels'), + MainThreadNotebookProxyKernels: createProxyIdentifier('MainThreadNotebookProxyKernels'), MainThreadNotebookRenderers: createProxyIdentifier('MainThreadNotebookRenderers'), MainThreadInteractive: createProxyIdentifier('MainThreadInteractive'), MainThreadTheming: createProxyIdentifier('MainThreadTheming'), @@ -2331,6 +2353,7 @@ export const ExtHostContext = { ExtHostNotebookDocuments: createProxyIdentifier('ExtHostNotebookDocuments'), ExtHostNotebookEditors: createProxyIdentifier('ExtHostNotebookEditors'), ExtHostNotebookKernels: createProxyIdentifier('ExtHostNotebookKernels'), + ExtHostNotebookProxyKernels: createProxyIdentifier('ExtHostNotebookProxyKernels'), ExtHostNotebookRenderers: createProxyIdentifier('ExtHostNotebookRenderers'), ExtHostInteractive: createProxyIdentifier('ExtHostInteractive'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index e4b2d776732..1fbc2d487de 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -9,9 +9,10 @@ import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, ViewColumn, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; +import { CustomEditorTabInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextTabInput, WebviewEditorTabInput } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { assertIsDefined } from 'vs/base/common/types'; +import { diffSets } from 'vs/base/common/collections'; export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { readonly _serviceBrand: undefined; @@ -47,7 +48,7 @@ class ExtHostEditorTab { get label() { return that._dto.label; }, - get kind() { + get input() { return that._input; }, get isDirty() { @@ -213,7 +214,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { private readonly _proxy: MainThreadEditorTabsShape; private readonly _onDidChangeTabs = new Emitter(); - private readonly _onDidChangeTabGroups = new Emitter(); + private readonly _onDidChangeTabGroups = new Emitter(); // Have to use ! because this gets initialized via an RPC proxy private _activeGroupId!: number; @@ -255,14 +256,14 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus); } }, - move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { - const extHostTab = this._findExtHostTabFromApi(tab); - if (!extHostTab) { - throw new Error('Invalid tab'); - } - this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); - return; - } + // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preservceFocus?: boolean) => { + // const extHostTab = this._findExtHostTabFromApi(tab); + // if (!extHostTab) { + // throw new Error('Invalid tab'); + // } + // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preservceFocus); + // return; + // } }; this._apiObject = Object.freeze(obj); } @@ -271,8 +272,22 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { + const groupIdsBefore = new Set(this._extHostTabGroups.map(group => group.groupId)); + const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId)); + const diff = diffSets(groupIdsBefore, groupIdsAfter); + + const closed: vscode.TabGroup[] = this._extHostTabGroups.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject); + const opened: vscode.TabGroup[] = []; + const changed: vscode.TabGroup[] = []; + + this._extHostTabGroups = tabGroups.map(tabGroup => { const group = new ExtHostEditorTabGroup(tabGroup, this._proxy, () => this._activeGroupId); + if (diff.added.includes(group.groupId)) { + opened.push(group.apiObject); + } else { + changed.push(group.apiObject); + } return group; }); @@ -281,7 +296,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) { this._activeGroupId = activeTabGroupId; } - this._onDidChangeTabGroups.fire(this._extHostTabGroups.map(g => g.apiObject)); + this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); } $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { @@ -293,7 +308,7 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { if (groupDto.isActive) { this._activeGroupId = groupDto.groupId; } - this._onDidChangeTabGroups.fire([group.apiObject]); + this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] })); } $acceptTabOperation(operation: TabOperation) { @@ -306,26 +321,26 @@ export class ExtHostEditorTabs implements IExtHostEditorTabs { // Construct the tab change event based on the operation switch (operation.kind) { case TabModelOperationKind.TAB_OPEN: - this._onDidChangeTabs.fire({ - added: [tab.apiObject], - removed: [], + this._onDidChangeTabs.fire(Object.freeze({ + opened: [tab.apiObject], + closed: [], changed: [] - }); + })); return; case TabModelOperationKind.TAB_CLOSE: - this._onDidChangeTabs.fire({ - added: [], - removed: [tab.apiObject], + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [tab.apiObject], changed: [] - }); + })); return; case TabModelOperationKind.TAB_MOVE: case TabModelOperationKind.TAB_UPDATE: - this._onDidChangeTabs.fire({ - added: [], - removed: [], + this._onDidChangeTabs.fire(Object.freeze({ + opened: [], + closed: [], changed: [tab.apiObject] - }); + })); return; } } diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index a60f54b98be..4b674cb4581 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -13,12 +13,12 @@ import { TernarySearchTree } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtHostExtensionServiceShape, MainContext, MainThreadExtensionServiceShape, MainThreadTelemetryShape, MainThreadWorkspaceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { IExtensionDescriptionDelta, IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ActivatedExtension, EmptyExtension, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; +import { MissingExtensionDependency, ActivationKind, checkProposedApiEnabled, isProposedApiEnabled, ExtensionActivationReason, extensionIdentifiersArrayToSet } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -100,12 +100,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme private readonly _readyToRunExtensions: Barrier; private readonly _eagerExtensionsActivated: Barrier; - protected readonly _registry: ExtensionDescriptionRegistry; + protected readonly _myRegistry: ExtensionDescriptionRegistry; + protected readonly _globalRegistry: ExtensionDescriptionRegistry; private readonly _storage: ExtHostStorage; private readonly _secretState: ExtHostSecretState; private readonly _storagePath: IExtensionStoragePaths; private readonly _activator: ExtensionsActivator; - private _extensionPathIndex: Promise> | null; + private _extensionPathIndex: Promise | null; private readonly _resolvers: { [authorityPrefix: string]: vscode.RemoteAuthorityResolver }; @@ -144,7 +145,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme this._readyToStartExtensionHost = new Barrier(); this._readyToRunExtensions = new Barrier(); this._eagerExtensionsActivated = new Barrier(); - this._registry = new ExtensionDescriptionRegistry(this._initData.extensions); + this._globalRegistry = new ExtensionDescriptionRegistry(this._initData.allExtensions); + const myExtensionsSet = extensionIdentifiersArrayToSet(this._initData.myExtensions); + this._myRegistry = new ExtensionDescriptionRegistry( + filterExtensions(this._globalRegistry, myExtensionsSet) + ); this._storage = new ExtHostStorage(this._extHostContext); this._secretState = new ExtHostSecretState(this._extHostContext); this._storagePath = storagePath; @@ -154,24 +159,33 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme [IExtHostSecretState, this._secretState] )); - const hostExtensions = new Set(); - this._initData.hostExtensions.forEach((extensionId) => hostExtensions.add(ExtensionIdentifier.toKey(extensionId))); + let resolvedExtensions: ExtensionIdentifier[] = []; + let hostExtensions: ExtensionIdentifier[] = []; + if (this._initData.remote.isRemote) { + resolvedExtensions = this._initData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); + hostExtensions = ( + this._initData.allExtensions + .filter(extension => !myExtensionsSet.has(ExtensionIdentifier.toKey(extension.identifier.value))) + .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) + ); + } + const hostExtensionsSet = extensionIdentifiersArrayToSet(hostExtensions); this._activator = this._register(new ExtensionsActivator( - this._registry, - this._initData.resolvedExtensions, - this._initData.hostExtensions, + this._myRegistry, + resolvedExtensions, + hostExtensions, { onExtensionActivationError: (extensionId: ExtensionIdentifier, error: Error, missingExtensionDependency: MissingExtensionDependency | null): void => { this._mainThreadExtensionsProxy.$onExtensionActivationError(extensionId, errors.transformErrorForSerialization(error), missingExtensionDependency); }, actualActivateExtension: async (extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise => { - if (hostExtensions.has(ExtensionIdentifier.toKey(extensionId))) { + if (hostExtensionsSet.has(ExtensionIdentifier.toKey(extensionId))) { await this._mainThreadExtensionsProxy.$activateExtension(extensionId, reason); return new HostExtension(); } - const extensionDescription = this._registry.getExtensionDescription(extensionId)!; + const extensionDescription = this._myRegistry.getExtensionDescription(extensionId)!; return this._activateExtension(extensionDescription, reason); } }, @@ -210,7 +224,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme let allPromises: Promise[] = []; try { - const allExtensions = this._registry.getAllExtensionDescriptions(); + const allExtensions = this._myRegistry.getAllExtensionDescriptions(); const allExtensionsIds = allExtensions.map(ext => ext.identifier); const activatedExtensions = allExtensionsIds.filter(id => this.isActivated(id)); @@ -293,7 +307,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } public getExtensionRegistry(): Promise { - return this._readyToRunExtensions.wait().then(_ => this._registry); + return this._readyToRunExtensions.wait().then(_ => this._myRegistry); } public getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined { @@ -316,28 +330,35 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } // create trie to enable fast 'filename -> extension id' look up - public async getExtensionPathIndex(): Promise> { + public async getExtensionPathIndex(): Promise { if (!this._extensionPathIndex) { - this._extensionPathIndex = (async () => { - const tst = TernarySearchTree.forUris(key => { - // using the default/biased extUri-util because the IExtHostFileSystemInfo-service - // isn't ready to be used yet, e.g the knowledge about `file` protocol and others - // comes in while this code runs - return extUriBiasedIgnorePathCase.ignorePathCasing(key); - }); - // const tst = TernarySearchTree.forUris(key => true); - for (const ext of this._registry.getAllExtensionDescriptions()) { - if (this._getEntryPoint(ext)) { - const uri = await this._realPathExtensionUri(ext.extensionLocation); - tst.set(uri, ext); - } - } - return tst; - })(); + this._extensionPathIndex = this._createExtensionPathIndex(this._myRegistry.getAllExtensionDescriptions()).then((searchTree) => { + return new ExtensionPaths(searchTree); + }); } return this._extensionPathIndex; } + /** + * create trie to enable fast 'filename -> extension id' look up + */ + private async _createExtensionPathIndex(extensions: IExtensionDescription[]): Promise> { + const tst = TernarySearchTree.forUris(key => { + // using the default/biased extUri-util because the IExtHostFileSystemInfo-service + // isn't ready to be used yet, e.g the knowledge about `file` protocol and others + // comes in while this code runs + return extUriBiasedIgnorePathCase.ignorePathCasing(key); + }); + // const tst = TernarySearchTree.forUris(key => true); + await Promise.all(this._myRegistry.getAllExtensionDescriptions().map(async (ext) => { + if (this._getEntryPoint(ext)) { + const uri = await this._realPathExtensionUri(ext.extensionLocation); + tst.set(uri, ext); + } + })); + return tst; + } + private _deactivate(extensionId: ExtensionIdentifier): Promise { let result = Promise.resolve(undefined); @@ -492,7 +513,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme get extensionMode() { return extensionMode; }, get extension() { if (extension === undefined) { - extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind); + extension = new Extension(that, extensionDescription.identifier, extensionDescription, extensionKind, false); } return extension; }, @@ -572,7 +593,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme // startup is considered finished this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks()); - for (const desc of this._registry.getAllExtensionDescriptions()) { + for (const desc of this._myRegistry.getAllExtensionDescriptions()) { if (desc.activationEvents) { for (const activationEvent of desc.activationEvents) { if (activationEvent === 'onStartupFinished') { @@ -607,7 +628,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme } return Promise.all( - this._registry.getAllExtensionDescriptions().map((desc) => { + this._myRegistry.getAllExtensionDescriptions().map((desc) => { return this._handleWorkspaceContainsEagerExtension(folders, desc); }) ).then(() => { }); @@ -816,8 +837,29 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return result; } - public $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise { - this._registry.keepOnly(enabledExtensionIds); + private static _applyExtensionsDelta(oldGlobalRegistry: ExtensionDescriptionRegistry, oldMyRegistry: ExtensionDescriptionRegistry, extensionsDelta: IExtensionDescriptionDelta) { + const globalRegistry = new ExtensionDescriptionRegistry(oldGlobalRegistry.getAllExtensionDescriptions()); + globalRegistry.deltaExtensions(extensionsDelta.toAdd, extensionsDelta.toRemove); + + const myExtensionsSet = extensionIdentifiersArrayToSet(oldMyRegistry.getAllExtensionDescriptions().map(extension => extension.identifier)); + for (const extensionId of extensionsDelta.myToRemove) { + myExtensionsSet.delete(ExtensionIdentifier.toKey(extensionId)); + } + for (const extensionId of extensionsDelta.myToAdd) { + myExtensionsSet.add(ExtensionIdentifier.toKey(extensionId)); + } + const myExtensions = filterExtensions(globalRegistry, myExtensionsSet); + + return { globalRegistry, myExtensions }; + } + + public $startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise { + extensionsDelta.toAdd.forEach((extension) => (extension).extensionLocation = URI.revive(extension.extensionLocation)); + + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); + return this._startExtensionHost(); } @@ -834,7 +876,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme public async $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise { await this._readyToRunExtensions.wait(); - if (!this._registry.getExtensionDescription(extensionId)) { + if (!this._myRegistry.getExtensionDescription(extensionId)) { // unknown extension => ignore return false; } @@ -842,24 +884,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return true; } - public async $deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { - toAdd.forEach((extension) => (extension).extensionLocation = URI.revive(extension.extensionLocation)); + public async $deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise { + extensionsDelta.toAdd.forEach((extension) => (extension).extensionLocation = URI.revive(extension.extensionLocation)); - const trie = await this.getExtensionPathIndex(); + // First build up and update the trie and only afterwards apply the delta + const { globalRegistry, myExtensions } = AbstractExtHostExtensionService._applyExtensionsDelta(this._globalRegistry, this._myRegistry, extensionsDelta); + const newSearchTree = await this._createExtensionPathIndex(myExtensions); + const extensionsPaths = await this.getExtensionPathIndex(); + extensionsPaths.setSearchTree(newSearchTree); + this._globalRegistry.set(globalRegistry.getAllExtensionDescriptions()); + this._myRegistry.set(myExtensions); - await Promise.all(toRemove.map(async (extensionId) => { - const extensionDescription = this._registry.getExtensionDescription(extensionId); - if (extensionDescription) { - trie.delete(await this._realPathExtensionUri(extensionDescription.extensionLocation)); - } - })); - - await Promise.all(toAdd.map(async (extensionDescription) => { - const realpathUri = await this._realPathExtensionUri(extensionDescription.extensionLocation); - trie.set(realpathUri, extensionDescription); - })); - - this._registry.deltaExtensions(toAdd, toRemove); return Promise.resolve(undefined); } @@ -930,7 +965,7 @@ export interface IExtHostExtensionService extends AbstractExtHostExtensionServic activateByIdWithErrors(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; getExtensionExports(extensionId: ExtensionIdentifier): IExtensionAPI | null | undefined; getExtensionRegistry(): Promise; - getExtensionPathIndex(): Promise>; + getExtensionPathIndex(): Promise; registerRemoteAuthorityResolver(authorityPrefix: string, resolver: vscode.RemoteAuthorityResolver): vscode.Disposable; onDidChangeRemoteConnectionData: Event; @@ -948,8 +983,9 @@ export class Extension implements vscode.Ex readonly extensionPath: string; readonly packageJSON: IExtensionDescription; readonly extensionKind: vscode.ExtensionKind; + readonly isFromDifferentExtensionHost: boolean; - constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind) { + constructor(extensionService: IExtHostExtensionService, originExtensionId: ExtensionIdentifier, description: IExtensionDescription, kind: ExtensionKind, isFromDifferentExtensionHost: boolean) { this.#extensionService = extensionService; this.#originExtensionId = originExtensionId; this.#identifier = description.identifier; @@ -958,24 +994,36 @@ export class Extension implements vscode.Ex this.extensionPath = path.normalize(originalFSPath(description.extensionLocation)); this.packageJSON = description; this.extensionKind = kind; + this.isFromDifferentExtensionHost = isFromDifferentExtensionHost; } get isActive(): boolean { + // TODO@alexdima support this return this.#extensionService.isActivated(this.#identifier); } get exports(): T { - if (this.packageJSON.api === 'none') { + if (this.packageJSON.api === 'none' || this.isFromDifferentExtensionHost) { return undefined!; // Strict nulloverride - Public api } return this.#extensionService.getExtensionExports(this.#identifier); } - activate(): Thenable { - return this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }).then(() => this.exports); + async activate(): Promise { + if (this.isFromDifferentExtensionHost) { + throw new Error('Cannot activate foreign extension'); // TODO@alexdima support this + } + await this.#extensionService.activateByIdWithErrors(this.#identifier, { startup: false, extensionId: this.#originExtensionId, activationEvent: 'api' }); + return this.exports; } } +function filterExtensions(globalRegistry: ExtensionDescriptionRegistry, desiredExtensions: Set): IExtensionDescription[] { + return globalRegistry.getAllExtensionDescriptions().filter( + extension => desiredExtensions.has(ExtensionIdentifier.toKey(extension.identifier)) + ); +} + function getRemoteAuthorityPrefix(remoteAuthority: string): string { const plusIndex = remoteAuthority.indexOf('+'); if (plusIndex === -1) { @@ -983,3 +1031,22 @@ function getRemoteAuthorityPrefix(remoteAuthority: string): string { } return remoteAuthority.substring(0, plusIndex); } + +export class ExtensionPaths { + + constructor( + private _searchTree: TernarySearchTree + ) { } + + setSearchTree(searchTree: TernarySearchTree): void { + this._searchTree = searchTree; + } + + findSubstr(key: URI): IExtensionDescription | undefined { + return this._searchTree.findSubstr(key); + } + + forEach(callback: (value: IExtensionDescription, index: URI) => any): void { + return this._searchTree.forEach(callback); + } +} diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 2cc57319c69..ef72e012647 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -42,13 +42,10 @@ import { Dto } from 'vs/workbench/services/extensions/common/proxyIdentifier'; class DocumentSymbolAdapter { - private _documents: ExtHostDocuments; - private _provider: vscode.DocumentSymbolProvider; - - constructor(documents: ExtHostDocuments, provider: vscode.DocumentSymbolProvider) { - this._documents = documents; - this._provider = provider; - } + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.DocumentSymbolProvider + ) { } async provideDocumentSymbols(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); @@ -256,7 +253,7 @@ class HoverAdapter { private readonly _provider: vscode.HoverProvider, ) { } - public async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise { + async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -464,7 +461,7 @@ class CodeActionAdapter { return { cacheId, actions }; } - public async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise { + async resolveCodeAction(id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise { const [sessionId, itemId] = id; const item = this._cache.get(sessionId, itemId); if (!item || CodeActionAdapter._isCommand(item)) { @@ -479,7 +476,7 @@ class CodeActionAdapter { : undefined; } - public releaseCodeActions(cachedId: number): void { + releaseCodeActions(cachedId: number): void { this._disposables.get(cachedId)?.dispose(); this._disposables.delete(cachedId); this._cache.delete(cachedId); @@ -697,8 +694,8 @@ class RenameAdapter { class SemanticTokensPreviousResult { constructor( - public readonly resultId: string | undefined, - public readonly tokens?: Uint32Array, + readonly resultId: string | undefined, + readonly tokens?: Uint32Array, ) { } } @@ -849,8 +846,7 @@ export class DocumentRangeSemanticTokensAdapter { constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.DocumentRangeSemanticTokensProvider, - ) { - } + ) { } async provideDocumentRangeSemanticTokens(resource: URI, range: IRange, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); @@ -870,7 +866,7 @@ export class DocumentRangeSemanticTokensAdapter { } } -class SuggestAdapter { +class CompletionsAdapter { static supportsResolving(provider: vscode.CompletionItemProvider): boolean { return typeof provider.resolveCompletionItem === 'function'; @@ -915,7 +911,7 @@ class SuggestAdapter { const list = Array.isArray(itemsOrList) ? new CompletionList(itemsOrList) : itemsOrList; // keep result for providers that support resolving - const pid: number = SuggestAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); + const pid: number = CompletionsAdapter.supportsResolving(this._provider) ? this._cache.add(list.items) : this._cache.add([]); const disposables = new DisposableStore(); this._disposables.set(pid, disposables); @@ -1027,9 +1023,13 @@ class SuggestAdapter { } class InlineCompletionAdapterBase { - public async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { return undefined; } + + disposeCompletions(pid: number): void { } + + handleDidShowCompletionItem(pid: number, idx: number): void { } } class InlineCompletionAdapter extends InlineCompletionAdapterBase { @@ -1044,7 +1044,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { super(); } - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1104,7 +1104,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } - public disposeCompletions(pid: number) { + override disposeCompletions(pid: number) { this._cache.delete(pid); const d = this._disposables.get(pid); if (d) { @@ -1113,7 +1113,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { this._disposables.delete(pid); } - public handleDidShowCompletionItem(pid: number, idx: number): void { + override handleDidShowCompletionItem(pid: number, idx: number): void { const completionItem = this._cache.get(pid, idx); if (completionItem) { InlineCompletionController.get(this._provider).fireOnDidShowCompletionItem({ @@ -1124,8 +1124,10 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { } class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { - private readonly _cache = new Cache('InlineCompletionItemNew'); - private readonly _disposables = new Map(); + private readonly _references = new ReferenceMap<{ + dispose(): void; + items: readonly vscode.InlineCompletionItemNew[]; + }>(); private readonly isAdditionProposedApiEnabled = isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions'); @@ -1143,7 +1145,7 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { [languages.InlineCompletionTriggerKind.Explicit]: InlineCompletionTriggerKindNew.Invoke, }; - public override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + override async provideInlineCompletions(resource: URI, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); @@ -1170,9 +1172,17 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { } const normalizedResult = isArray(result) ? result : result.items; + const commands = isArray(result) ? [] : result.commands || []; - const pid = this._cache.add(normalizedResult); let disposableStore: DisposableStore | undefined = undefined; + const pid = this._references.createReferenceId({ + dispose() { + if (disposableStore) { + disposableStore.dispose(); + } + }, + items: normalizedResult + }); return { pid, @@ -1181,7 +1191,6 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { if (item.command) { if (!disposableStore) { disposableStore = new DisposableStore(); - this._disposables.set(pid, disposableStore); } command = this._commands.toInternal(item.command, disposableStore); } @@ -1196,32 +1205,55 @@ class InlineCompletionAdapterNew extends InlineCompletionAdapterBase { completeBracketPairs: this.isAdditionProposedApiEnabled ? item.completeBracketPairs : false }); }), + commands: commands.map(c => { + if (!disposableStore) { + disposableStore = new DisposableStore(); + } + return this._commands.toInternal(c, disposableStore); + }) }; } - public disposeCompletions(pid: number) { - this._cache.delete(pid); - const d = this._disposables.get(pid); - if (d) { - d.clear(); - } - this._disposables.delete(pid); + override disposeCompletions(pid: number) { + const data = this._references.disposeReferenceId(pid); + data?.dispose(); } - public handleDidShowCompletionItem(pid: number, idx: number): void { - const completionItem = this._cache.get(pid, idx); + override handleDidShowCompletionItem(pid: number, idx: number): void { + const completionItem = this._references.get(pid)?.items[idx]; if (completionItem) { - if (this._provider.handleDidShowCompletionItem && isProposedApiEnabled(this.extension, 'inlineCompletionsAdditions')) { + if (this._provider.handleDidShowCompletionItem && this.isAdditionProposedApiEnabled) { this._provider.handleDidShowCompletionItem(completionItem); } } } } +class ReferenceMap { + private readonly _references = new Map(); + private _idPool = 1; + + createReferenceId(value: T): number { + const id = this._idPool++; + this._references.set(id, value); + return id; + } + + disposeReferenceId(referenceId: number): T | undefined { + const value = this._references.get(referenceId); + this._references.delete(referenceId); + return value; + } + + get(referenceId: number): T | undefined { + return this._references.get(referenceId); + } +} + export class InlineCompletionController implements vscode.InlineCompletionController { private static readonly map = new WeakMap, InlineCompletionController>(); - public static get(provider: vscode.InlineCompletionItemProvider): InlineCompletionController { + static get(provider: vscode.InlineCompletionItemProvider): InlineCompletionController { let existing = InlineCompletionController.map.get(provider); if (!existing) { existing = new InlineCompletionController(); @@ -1231,9 +1263,9 @@ export class InlineCompletionController i } private readonly _onDidShowCompletionItemEmitter = new Emitter>(); - public readonly onDidShowCompletionItem: vscode.Event> = this._onDidShowCompletionItemEmitter.event; + readonly onDidShowCompletionItem: vscode.Event> = this._onDidShowCompletionItemEmitter.event; - public fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent): void { + fireOnDidShowCompletionItem(event: vscode.InlineCompletionItemDidShowEvent): void { this._onDidShowCompletionItemEmitter.fire(event); } } @@ -1738,7 +1770,7 @@ class DocumentOnDropAdapter { type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter + | CompletionsAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | TypeHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter @@ -2158,21 +2190,21 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { - const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); - this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); + const handle = this._addNewAdapter(new CompletionsAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); + this._proxy.$registerCompletionsProvider(handle, this._transformDocumentSelector(selector), triggerCharacters, CompletionsAdapter.supportsResolving(provider), `${extension.identifier.value}(${triggerCharacters.join('')})`); return this._createDisposable(handle); } $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: languages.CompletionContext, token: CancellationToken): Promise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.provideCompletionItems(URI.revive(resource), position, context, token), undefined, token); } $resolveCompletionItem(handle: number, id: extHostProtocol.ChainedCacheId, token: CancellationToken): Promise { - return this._withAdapter(handle, SuggestAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); + return this._withAdapter(handle, CompletionsAdapter, adapter => adapter.resolveCompletionItem(id, token), undefined, token); } $releaseCompletionItems(handle: number, id: number): void { - this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); + this._withAdapter(handle, CompletionsAdapter, adapter => adapter.releaseCompletionItems(id), undefined, undefined); } // --- ghost test @@ -2194,13 +2226,13 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF } $handleInlineCompletionDidShow(handle: number, pid: number, idx: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.handleDidShowCompletionItem(pid, idx); }, undefined, undefined); } $freeInlineCompletionsList(handle: number, pid: number): void { - this._withAdapter(handle, InlineCompletionAdapter, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); + this._withAdapter(handle, InlineCompletionAdapterBase, async adapter => { adapter.disposeCompletions(pid); }, undefined, undefined); } // --- parameter hints diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 6d68184d52c..7c9cc4c0eb0 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -523,12 +523,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const commandDataToNotebook = new ApiCommand( 'vscode.executeDataToNotebook', '_executeDataToNotebook', 'Invoke notebook serializer', [notebookTypeArg, new ApiCommandArgument('data', 'Bytes to convert to data', v => v instanceof Uint8Array, v => VSBuffer.wrap(v))], - new ApiCommandResult('Notebook Data', dto => typeConverters.NotebookData.to(dto)) + new ApiCommandResult, vscode.NotebookData>('Notebook Data', data => typeConverters.NotebookData.to(data.value)) ); const commandNotebookToData = new ApiCommand( 'vscode.executeNotebookToData', '_executeNotebookToData', 'Invoke notebook serializer', - [notebookTypeArg, new ApiCommandArgument('NotebookData', 'Notebook data to convert to bytes', v => true, v => typeConverters.NotebookData.from(v))], + [notebookTypeArg, new ApiCommandArgument>('NotebookData', 'Notebook data to convert to bytes', v => true, v => new SerializableObjectWithBuffers(typeConverters.NotebookData.from(v)))], new ApiCommandResult('Bytes', dto => dto.buffer) ); diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts deleted file mode 100644 index 65ecebc97c1..00000000000 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ /dev/null @@ -1,192 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as types from 'vs/workbench/api/common/extHostTypes'; -import * as vscode from 'vscode'; -import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { score } from 'vs/editor/common/languageSelector'; -import { ResourceMap } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; - -export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextDocument { - - private _disposables = new DisposableStore(); - private _isClosed = false; - - private _cells!: vscode.NotebookCell[]; - private _cellUris!: ResourceMap; - private _cellLengths!: PrefixSumComputer; - private _cellLines!: PrefixSumComputer; - private _versionId = 0; - - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - - readonly uri = URI.from({ scheme: 'vscode-concat-doc', path: generateUuid() }); - - constructor( - extHostNotebooks: ExtHostNotebookDocuments, - extHostDocuments: ExtHostDocuments, - private readonly _notebook: vscode.NotebookDocument, - private readonly _selector: vscode.DocumentSelector | undefined, - ) { - this._init(); - - this._disposables.add(extHostDocuments.onDidChangeDocument(e => { - const cellIdx = this._cellUris.get(e.document.uri); - if (cellIdx !== undefined) { - this._cellLengths.setValue(cellIdx, this._cells[cellIdx].document.getText().length + 1); - this._cellLines.setValue(cellIdx, this._cells[cellIdx].document.lineCount); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - })); - const documentChange = (document: vscode.NotebookDocument) => { - if (document === this._notebook) { - this._init(); - this._versionId += 1; - this._onDidChange.fire(undefined); - } - }; - - this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => documentChange(e.notebook))); - } - - dispose(): void { - this._disposables.dispose(); - this._isClosed = true; - } - - get isClosed() { - return this._isClosed; - } - - private _init() { - this._cells = []; - this._cellUris = new ResourceMap(); - const cellLengths: number[] = []; - const cellLineCounts: number[] = []; - for (const cell of this._notebook.getCells()) { - if (cell.kind === types.NotebookCellKind.Code && (!this._selector || score(this._selector, cell.document.uri, cell.document.languageId, true, undefined))) { - this._cellUris.set(cell.document.uri, this._cells.length); - this._cells.push(cell); - cellLengths.push(cell.document.getText().length + 1); - cellLineCounts.push(cell.document.lineCount); - } - } - this._cellLengths = new PrefixSumComputer(new Uint32Array(cellLengths)); - this._cellLines = new PrefixSumComputer(new Uint32Array(cellLineCounts)); - } - - get version(): number { - return this._versionId; - } - - getText(range?: vscode.Range): string { - if (!range) { - let result = ''; - for (const cell of this._cells) { - result += cell.document.getText() + '\n'; - } - // remove last newline again - result = result.slice(0, -1); - return result; - } - - if (range.isEmpty) { - return ''; - } - - // get start and end locations and create substrings - const start = this.locationAt(range.start); - const end = this.locationAt(range.end); - - const startIdx = this._cellUris.get(start.uri); - const endIdx = this._cellUris.get(end.uri); - - if (startIdx === undefined || endIdx === undefined) { - return ''; - } - - if (startIdx === endIdx) { - return this._cells[startIdx].document.getText(new types.Range(start.range.start, end.range.end)); - } - - const parts = [this._cells[startIdx].document.getText(new types.Range(start.range.start, new types.Position(this._cells[startIdx].document.lineCount, 0)))]; - for (let i = startIdx + 1; i < endIdx; i++) { - parts.push(this._cells[i].document.getText()); - } - parts.push(this._cells[endIdx].document.getText(new types.Range(new types.Position(0, 0), end.range.end))); - return parts.join('\n'); - } - - offsetAt(position: vscode.Position): number { - const idx = this._cellLines.getIndexOf(position.line); - const offset1 = this._cellLengths.getPrefixSum(idx.index - 1); - const offset2 = this._cells[idx.index].document.offsetAt(position.with(idx.remainder)); - return offset1 + offset2; - } - - positionAt(locationOrOffset: vscode.Location | number): vscode.Position { - if (typeof locationOrOffset === 'number') { - const idx = this._cellLengths.getIndexOf(locationOrOffset); - const lineCount = this._cellLines.getPrefixSum(idx.index - 1); - return this._cells[idx.index].document.positionAt(idx.remainder).translate(lineCount); - } - - const idx = this._cellUris.get(locationOrOffset.uri); - if (idx !== undefined) { - const line = this._cellLines.getPrefixSum(idx - 1); - return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character); - } - // do better? - // return undefined; - return new types.Position(0, 0); - } - - locationAt(positionOrRange: vscode.Range | vscode.Position): types.Location { - if (!types.Range.isRange(positionOrRange)) { - positionOrRange = new types.Range(positionOrRange, positionOrRange); - } - - const startIdx = this._cellLines.getIndexOf(positionOrRange.start.line); - let endIdx = startIdx; - if (!positionOrRange.isEmpty) { - endIdx = this._cellLines.getIndexOf(positionOrRange.end.line); - } - - const startPos = new types.Position(startIdx.remainder, positionOrRange.start.character); - const endPos = new types.Position(endIdx.remainder, positionOrRange.end.character); - const range = new types.Range(startPos, endPos); - - const startCell = this._cells[startIdx.index]; - return new types.Location(startCell.document.uri, startCell.document.validateRange(range)); - } - - contains(uri: vscode.Uri): boolean { - return this._cellUris.has(uri); - } - - validateRange(range: vscode.Range): vscode.Range { - const start = this.validatePosition(range.start); - const end = this.validatePosition(range.end); - return range.with(start, end); - } - - validatePosition(position: vscode.Position): vscode.Position { - const startIdx = this._cellLines.getIndexOf(position.line); - - const cellPosition = new types.Position(startIdx.remainder, position.character); - const validCellPosition = this._cells[startIdx.index].document.validatePosition(cellPosition); - - const line = this._cellLines.getPrefixSum(startIdx.index - 1); - return new types.Position(line + validCellPosition.line, validCellPosition.character); - } -} diff --git a/src/vs/workbench/api/common/extHostNotebookDocument.ts b/src/vs/workbench/api/common/extHostNotebookDocument.ts index e8cc8e027ff..b77133347e3 100644 --- a/src/vs/workbench/api/common/extHostNotebookDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookDocument.ts @@ -218,11 +218,11 @@ export class ExtHostNotebookDocument { const result = { notebook: this.apiNotebook, metadata: newMetadata, - cellChanges: [], + cellChanges: [], contentChanges: [], }; - type RelaxedCellChange = Partial & { cell: vscode.NotebookCell }; + type RelaxedCellChange = Partial & { cell: vscode.NotebookCell }; const relaxedCellChanges: RelaxedCellChange[] = []; // -- apply change and populate content changes diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 05550b53475..f99ead59ac7 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -108,7 +108,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor; message: any }>(); const data: INotebookKernelDto2 = { - id: createKernelId(extension, id), + id: createKernelId(extension.identifier, id), notebookType: viewType, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, @@ -218,7 +218,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { that._logService.trace(`NotebookController[${handle}] NOT associated to notebook, associated to THESE notebooks:`, Array.from(associatedNotebooks.keys()).map(u => u.toString())); throw new Error(`notebook controller is NOT associated to notebook: ${cell.notebook.uri.toString()}`); } - return that._createNotebookCellExecution(cell, createKernelId(extension, this.id)); + return that._createNotebookCellExecution(cell, createKernelId(extension.identifier, this.id)); }, dispose: () => { if (!isDisposed) { @@ -257,6 +257,15 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return controller; } + getIdByController(controller: vscode.NotebookController) { + for (const [_, candidate] of this._kernelData) { + if (candidate.controller === controller) { + return createKernelId(candidate.extensionId, controller.id); + } + } + return null; + } + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { @@ -583,6 +592,6 @@ class TimeoutBasedCollector { } } -function createKernelId(extension: IExtensionDescription, id: string): string { - return `${extension.identifier.value}/${id}`; +export function createKernelId(extensionIdentifier: ExtensionIdentifier, id: string): string { + return `${extensionIdentifier.value}/${id}`; } diff --git a/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts new file mode 100644 index 00000000000..786101bf360 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebookProxyKernels.ts @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ExtHostNotebookProxyKernelsShape, IMainContext, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; +import { createKernelId, ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import * as vscode from 'vscode'; + +interface IProxyKernelData { + extensionId: ExtensionIdentifier; + controller: vscode.NotebookProxyController; + onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>; + associatedNotebooks: ResourceMap; +} + +export type SelectKernelReturnArgs = ControllerInfo | { notebookEditorId: string } | ControllerInfo & { notebookEditorId: string } | undefined; +type ControllerInfo = { id: string; extension: string }; + + +export class ExtHostNotebookProxyKernels implements ExtHostNotebookProxyKernelsShape { + + private readonly _proxy: MainThreadNotebookProxyKernelsShape; + + private readonly _proxyKernelData: Map = new Map(); + private _handlePool: number = 0; + + private readonly _onDidChangeCellExecutionState = new Emitter(); + readonly onDidChangeNotebookCellExecutionState = this._onDidChangeCellExecutionState.event; + + constructor( + mainContext: IMainContext, + private readonly extHostNotebook: ExtHostNotebookKernels, + @ILogService private readonly _logService: ILogService + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookProxyKernels); + } + + createNotebookProxyController(extension: IExtensionDescription, id: string, viewType: string, label: string, handler: () => vscode.NotebookController | string | Thenable): vscode.NotebookProxyController { + const handle = this._handlePool++; + + let isDisposed = false; + const commandDisposables = new DisposableStore(); + const onDidChangeSelection = new Emitter<{ selected: boolean; notebook: vscode.NotebookDocument }>(); + + const data: INotebookProxyKernelDto = { + id: createKernelId(extension.identifier, id), + notebookType: viewType, + extensionId: extension.identifier, + extensionLocation: extension.extensionLocation, + label: label || extension.identifier.value, + }; + + let _resolveHandler = handler; + + this._proxy.$addProxyKernel(handle, data).catch(err => { + // this can happen when a kernel with that ID is already registered + console.log(err); + isDisposed = true; + }); + + let tokenPool = 0; + const _update = () => { + if (isDisposed) { + return; + } + const myToken = ++tokenPool; + Promise.resolve().then(() => { + if (myToken === tokenPool) { + this._proxy.$updateProxyKernel(handle, data); + } + }); + }; + + // notebook documents that are associated to this controller + const associatedNotebooks = new ResourceMap(); + + const controller: vscode.NotebookProxyController = { + get id() { return id; }, + get notebookType() { return data.notebookType; }, + onDidChangeSelectedNotebooks: onDidChangeSelection.event, + get label() { + return data.label; + }, + set label(value) { + data.label = value ?? extension.displayName ?? extension.name; + _update(); + }, + get detail() { + return data.detail ?? ''; + }, + set detail(value) { + data.detail = value; + _update(); + }, + get description() { + return data.description ?? ''; + }, + set description(value) { + data.description = value; + _update(); + }, + get kind() { + checkProposedApiEnabled(extension, 'notebookControllerKind'); + return data.kind ?? ''; + }, + set kind(value) { + checkProposedApiEnabled(extension, 'notebookControllerKind'); + data.kind = value; + _update(); + }, + get resolveHandler() { + return _resolveHandler; + }, + dispose: () => { + if (!isDisposed) { + this._logService.trace(`NotebookProxyController[${handle}], DISPOSED`); + isDisposed = true; + this._proxyKernelData.delete(handle); + commandDisposables.dispose(); + onDidChangeSelection.dispose(); + this._proxy.$removeProxyKernel(handle); + } + } + }; + + this._proxyKernelData.set(handle, { + extensionId: extension.identifier, + controller, + onDidChangeSelection, + associatedNotebooks + }); + return controller; + } + + async $resolveKernel(handle: number): Promise { + const obj = this._proxyKernelData.get(handle); + if (!obj) { + // extension can dispose kernels in the meantime + return null; + } + + const controller = await obj.controller.resolveHandler(); + if (typeof controller === 'string') { + return controller; + } else { + return this.extHostNotebook.getIdByController(controller); + } + } +} + diff --git a/src/vs/workbench/api/common/extHostOutput.ts b/src/vs/workbench/api/common/extHostOutput.ts index 12e6570161e..5c013fa2534 100644 --- a/src/vs/workbench/api/common/extHostOutput.ts +++ b/src/vs/workbench/api/common/extHostOutput.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; diff --git a/src/vs/workbench/api/common/extHostQuickOpen.ts b/src/vs/workbench/api/common/extHostQuickOpen.ts index 0df03b79137..a3c48d6cc54 100644 --- a/src/vs/workbench/api/common/extHostQuickOpen.ts +++ b/src/vs/workbench/api/common/extHostQuickOpen.ts @@ -3,16 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostWorkspaceProvider } from 'vs/workbench/api/common/extHostWorkspace'; -import type { InputBox, InputBoxOptions, InputBoxValidationSeverity, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; +import { InputBox, InputBoxOptions, InputBoxValidationMessage, QuickInput, QuickInputButton, QuickPick, QuickPickItem, QuickPickItemButtonEvent, QuickPickOptions, WorkspaceFolder, WorkspaceFolderPickOptions } from 'vscode'; import { ExtHostQuickOpenShape, IMainContext, MainContext, TransferQuickInput, TransferQuickInputButton, TransferQuickPickItemOrSeparator } from './extHost.protocol'; import { URI } from 'vs/base/common/uri'; -import { ThemeIcon, QuickInputButtons, QuickPickItemKind } from 'vs/workbench/api/common/extHostTypes'; +import { ThemeIcon, QuickInputButtons, QuickPickItemKind, InputBoxValidationSeverity } from 'vs/workbench/api/common/extHostTypes'; import { isCancellationError } from 'vs/base/common/errors'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { coalesce } from 'vs/base/common/arrays'; @@ -46,7 +45,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _commands: ExtHostCommands; private _onDidSelectItem?: (handle: number) => void; - private _validateInput?: (input: string) => string | { content: string; severity: Severity } | undefined | null | Thenable; + private _validateInput?: (input: string) => string | InputBoxValidationMessage | undefined | null | Thenable; private _sessions = new Map(); @@ -148,7 +147,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx showInput(options?: InputBoxOptions, token: CancellationToken = CancellationToken.None): Promise { // global validate fn used in callback below - this._validateInput = options ? options.validateInput : undefined; + this._validateInput = options ? options.validateInput2 ?? options.validateInput : undefined; return proxy.$input(options, typeof this._validateInput === 'function', token) .then(undefined, err => { @@ -160,11 +159,36 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx }); } - $validateInput(input: string): Promise { - if (this._validateInput) { - return asPromise(() => this._validateInput!(input)); + async $validateInput(input: string): Promise { + if (!this._validateInput) { + return; } - return Promise.resolve(undefined); + + const result = await this._validateInput(input); + if (!result || typeof result === 'string') { + return result; + } + + let severity: Severity; + switch (result.severity) { + case InputBoxValidationSeverity.Info: + severity = Severity.Info; + break; + case InputBoxValidationSeverity.Warning: + severity = Severity.Warning; + break; + case InputBoxValidationSeverity.Error: + severity = Severity.Error; + break; + default: + severity = result.message ? Severity.Error : Severity.Ignore; + break; + } + + return { + content: result.message, + severity + }; } // ---- workspace folder picker @@ -675,7 +699,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx private _password = false; private _prompt: string | undefined; private _validationMessage: string | undefined; - private _validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; + private _validationMessage2: string | InputBoxValidationMessage | undefined; constructor(private readonly extension: IExtensionDescription, onDispose: () => void) { super(extension.identifier, onDispose); @@ -713,7 +737,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx return this._validationMessage2; } - set validationMessage2(validationMessage: string | { content: string; severity: InputBoxValidationSeverity } | undefined) { + set validationMessage2(validationMessage: string | InputBoxValidationMessage | undefined) { checkProposedApiEnabled(this.extension, 'inputBoxSeverity'); this._validationMessage2 = validationMessage; if (!validationMessage) { @@ -721,7 +745,7 @@ export function createExtHostQuickOpen(mainContext: IMainContext, workspace: IEx } else if (typeof validationMessage === 'string') { this.update({ validationMessage, severity: Severity.Error }); } else { - this.update({ validationMessage: validationMessage.content, severity: validationMessage.severity ?? Severity.Error }); + this.update({ validationMessage: validationMessage.message, severity: validationMessage.severity ?? Severity.Error }); } } } diff --git a/src/vs/workbench/api/common/extHostRequireInterceptor.ts b/src/vs/workbench/api/common/extHostRequireInterceptor.ts index 9ce9a8f402f..4a072952392 100644 --- a/src/vs/workbench/api/common/extHostRequireInterceptor.ts +++ b/src/vs/workbench/api/common/extHostRequireInterceptor.ts @@ -4,19 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as performance from 'vs/base/common/performance'; -import { TernarySearchTree } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as vscode from 'vscode'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { IExtensionApiFactory } from 'vs/workbench/api/common/extHost.api.impl'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionApiFactory, IExtensionRegistries } from 'vs/workbench/api/common/extHost.api.impl'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; +import { ExtensionPaths, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { platform } from 'vs/base/common/process'; import { ILogService } from 'vs/platform/log/common/log'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; @@ -42,7 +40,7 @@ export abstract class RequireInterceptor { constructor( private _apiFactory: IExtensionApiFactory, - private _extensionRegistry: ExtensionDescriptionRegistry, + private _extensionRegistry: IExtensionRegistries, @IInstantiationService private readonly _instaService: IInstantiationService, @IExtHostConfiguration private readonly _extHostConfiguration: IExtHostConfiguration, @IExtHostExtensionService private readonly _extHostExtensionService: IExtHostExtensionService, @@ -156,8 +154,8 @@ class VSCodeNodeModuleFactory implements INodeModuleFactory { constructor( private readonly _apiFactory: IExtensionApiFactory, - private readonly _extensionPaths: TernarySearchTree, - private readonly _extensionRegistry: ExtensionDescriptionRegistry, + private readonly _extensionPaths: ExtensionPaths, + private readonly _extensionRegistry: IExtensionRegistries, private readonly _configProvider: ExtHostConfigProvider, private readonly _logService: ILogService, ) { @@ -208,7 +206,7 @@ class KeytarNodeModuleFactory implements INodeModuleFactory { private _impl: IKeytarModule; constructor( - private readonly _extensionPaths: TernarySearchTree, + private readonly _extensionPaths: ExtensionPaths, @IExtHostRpcService rpcService: IExtHostRpcService, @IExtHostInitDataService initData: IExtHostInitDataService, @@ -303,7 +301,7 @@ class OpenNodeModuleFactory implements INodeModuleFactory { private _mainThreadTelemetry: MainThreadTelemetryShape; constructor( - private readonly _extensionPaths: TernarySearchTree, + private readonly _extensionPaths: ExtensionPaths, private readonly _appUriScheme: string, @IExtHostRpcService rpcService: IExtHostRpcService, ) { diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index baab688ad31..c9d9e611649 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Timeline, TimelineItem, TimelineOptions, TimelineProvider, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -19,7 +19,7 @@ import { isString } from 'vs/base/common/types'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise; + $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise; } export const IExtHostTimeline = createDecorator('IExtHostTimeline'); @@ -51,9 +51,9 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise { + async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken): Promise { const provider = this._providers.get(id); - return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions); + return provider?.provideTimeline(URI.revive(uri), options, token); } registerTimelineProvider(scheme: string | string[], provider: vscode.TimelineProvider, _extensionId: ExtensionIdentifier, commandConverter: CommandsConverter): IDisposable { @@ -71,8 +71,8 @@ export class ExtHostTimeline implements IExtHostTimeline { ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { - if (internalOptions?.resetCache) { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken) { + if (options?.resetCache) { timelineDisposables.clear(); // For now, only allow the caching of a single Uri @@ -87,7 +87,7 @@ export class ExtHostTimeline implements IExtHostTimeline { // TODO: Should we bother converting all the data if we aren't caching? Meaning it is being requested by an extension? - const convertItem = convertTimelineItem(uri, internalOptions); + const convertItem = convertTimelineItem(uri, options); return { ...result, source: provider.id, @@ -106,7 +106,7 @@ export class ExtHostTimeline implements IExtHostTimeline { } private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) { - return (uri: URI, options?: InternalTimelineOptions) => { + return (uri: URI, options?: TimelineOptions) => { let items: Map | undefined; if (options?.cacheResults) { let itemsByUri = this._itemsBySourceAndUriMap.get(source); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 9dec8f6b163..979eebec86c 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -7,6 +7,7 @@ import { asArray, coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { VSBuffer } from 'vs/base/common/buffer'; import * as htmlContent from 'vs/base/common/htmlContent'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ResourceSet } from 'vs/base/common/map'; import { marked } from 'vs/base/common/marked/marked'; import { parse } from 'vs/base/common/marshalling'; import { cloneAndChange } from 'vs/base/common/objects'; @@ -580,7 +581,17 @@ export namespace WorkspaceEdit { }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value._allEntries()) { + + // collect all files that are to be created so that their version + // information (in case they exist as text model already) can be ignored + const toCreate = new ResourceSet(); + for (const entry of value._allEntries()) { + if (entry._type === types.FileEditType.File && URI.isUri(entry.to) && entry.from === undefined) { + toCreate.add(entry.to); + } + } + + for (const entry of value._allEntries()) { if (entry._type === types.FileEditType.File) { // file operation @@ -598,7 +609,7 @@ export namespace WorkspaceEdit { _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), - modelVersionId: versionInfo?.getTextDocumentVersion(entry.uri), + modelVersionId: !toCreate.has(entry.uri) ? versionInfo?.getTextDocumentVersion(entry.uri) : undefined, metadata: entry.metadata }); } else if (entry._type === types.FileEditType.Cell) { @@ -1480,7 +1491,8 @@ export namespace LanguageSelector { language: filter.language, scheme: filter.scheme, pattern: GlobPattern.from(filter.pattern), - exclusive: filter.exclusive + exclusive: filter.exclusive, + notebookType: filter.notebookType }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 8ed1d005b1a..a69e00f05ec 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1643,8 +1643,11 @@ export class InlineSuggestionNew implements vscode.InlineCompletionItemNew { export class InlineSuggestionsNew implements vscode.InlineCompletionListNew { items: vscode.InlineCompletionItemNew[]; - constructor(items: vscode.InlineCompletionItemNew[]) { + commands: vscode.Command[] | undefined; + + constructor(items: vscode.InlineCompletionItemNew[], commands?: vscode.Command[]) { this.items = items; + this.commands = commands; } } diff --git a/src/vs/workbench/api/common/extHostVariableResolverService.ts b/src/vs/workbench/api/common/extHostVariableResolverService.ts index a992dd05f77..7455de15e22 100644 --- a/src/vs/workbench/api/common/extHostVariableResolverService.ts +++ b/src/vs/workbench/api/common/extHostVariableResolverService.ts @@ -50,10 +50,10 @@ class ExtHostVariableResolverService extends AbstractVariableResolverService { const activeTab = editorTabs.tabGroups.all.find(group => group.isActive)?.activeTab; if (activeTab !== undefined) { // Resolve a resource from the tab - if (activeTab.kind instanceof TextDiffTabInput || activeTab.kind instanceof NotebookDiffEditorTabInput) { - return activeTab.kind.modified; - } else if (activeTab.kind instanceof TextTabInput || activeTab.kind instanceof NotebookEditorTabInput || activeTab.kind instanceof CustomEditorTabInput) { - return activeTab.kind.uri; + if (activeTab.input instanceof TextDiffTabInput || activeTab.input instanceof NotebookDiffEditorTabInput) { + return activeTab.input.modified; + } else if (activeTab.input instanceof TextTabInput || activeTab.input instanceof NotebookEditorTabInput || activeTab.input instanceof CustomEditorTabInput) { + return activeTab.input.uri; } } } diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 99c7c7b08fd..0eabc9e29cc 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -120,7 +120,7 @@ export class ExtensionHostMain { } private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { - initData.extensions.forEach((ext) => (ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); + initData.allExtensions.forEach((ext) => (ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation))); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); const extDevLocs = initData.environment.extensionDevelopmentLocationURI; if (extDevLocs) { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 481409b79d5..62e0d6b4998 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -72,7 +72,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } // Module loading tricks - const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry); + const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await interceptor.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts index 1f7d249f197..1e385fc213f 100644 --- a/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts +++ b/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts @@ -7,7 +7,7 @@ import type * as vscode from 'vscode'; import assert = require('assert'); import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; -import { IEditorTabDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { SingleProxyRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; import { TextTabInput } from 'vs/workbench/api/common/extHostTypes'; @@ -126,6 +126,60 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(count, 1); }); + test('Check TabGroupChangeEvent properties', function () { + const extHostEditorTabs = new ExtHostEditorTabs( + SingleProxyRPCProtocol(new class extends mock() { + // override/implement $moveTab or $closeTab + }) + ); + + const group1Data: IEditorTabGroupDto = { + isActive: true, + viewColumn: 0, + groupId: 12, + tabs: [] + }; + const group2Data: IEditorTabGroupDto = { ...group1Data, groupId: 13 }; + + const events: vscode.TabGroupChangeEvent[] = []; + extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e)); + // OPEN + extHostEditorTabs.$acceptEditorTabModel([group1Data]); + assert.deepStrictEqual(events, [{ + changed: [], + closed: [], + opened: [extHostEditorTabs.tabGroups.activeTabGroup] + }]); + + // OPEN, CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([{ ...group1Data, isActive: false }, group2Data]); + assert.deepStrictEqual(events, [{ + changed: [extHostEditorTabs.tabGroups.all[0]], + closed: [], + opened: [extHostEditorTabs.tabGroups.all[1]] + }]); + + // CHANGE + events.length = 0; + extHostEditorTabs.$acceptEditorTabModel([group1Data, { ...group2Data, isActive: false }]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [], + opened: [] + }]); + + // CLOSE, CHANGE + events.length = 0; + const oldActiveGroup = extHostEditorTabs.tabGroups.activeTabGroup; + extHostEditorTabs.$acceptEditorTabModel([group2Data]); + assert.deepStrictEqual(events, [{ + changed: extHostEditorTabs.tabGroups.all, + closed: [oldActiveGroup], + opened: [] + }]); + }); + test('Ensure reference equality for activeTab and activeGroup', function () { const extHostEditorTabs = new ExtHostEditorTabs( SingleProxyRPCProtocol(new class extends mock() { @@ -175,10 +229,10 @@ suite('ExtHostEditorTabs', function () { let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab1 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); + assert.ok(apiTab1.input instanceof TextTabInput); assert.strictEqual(tabDto.input.kind, TabInputKind.TextInput); const dtoResource = (tabDto.input as TextInputDto).uri; - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab1.isDirty, true); @@ -196,8 +250,8 @@ suite('ExtHostEditorTabs', function () { all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat(); assert.strictEqual(all.length, 1); const apiTab2 = all[0]; - assert.ok(apiTab1.kind instanceof TextTabInput); - assert.strictEqual(apiTab1.kind.uri.toString(), URI.revive(dtoResource).toString()); + assert.ok(apiTab1.input instanceof TextTabInput); + assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString()); assert.strictEqual(apiTab2.isDirty, false); assert.strictEqual(apiTab1 === apiTab2, true); @@ -243,10 +297,10 @@ suite('ExtHostEditorTabs', function () { assert.strictEqual(all.length, 2); const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab1?.kind instanceof TextTabInput); + assert.ok(activeTab1?.input instanceof TextTabInput); assert.strictEqual(tabDtoAAA.input.kind, TabInputKind.TextInput); const dtoAAAResource = (tabDtoAAA.input as TextInputDto).uri; - assert.strictEqual(activeTab1?.kind?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); + assert.strictEqual(activeTab1?.input?.uri.toString(), URI.revive(dtoAAAResource)?.toString()); assert.strictEqual(activeTab1?.isActive, true); extHostEditorTabs.$acceptTabOperation({ @@ -257,10 +311,10 @@ suite('ExtHostEditorTabs', function () { }); const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab; - assert.ok(activeTab2?.kind instanceof TextTabInput); + assert.ok(activeTab2?.input instanceof TextTabInput); assert.strictEqual(tabDtoBBB.input.kind, TabInputKind.TextInput); const dtoBBBResource = (tabDtoBBB.input as TextInputDto).uri; - assert.strictEqual(activeTab2?.kind?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); + assert.strictEqual(activeTab2?.input?.uri.toString(), URI.revive(dtoBBBResource)?.toString()); assert.strictEqual(activeTab2?.isActive, true); assert.strictEqual(activeTab1?.isActive, false); }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts deleted file mode 100644 index 6267d249941..00000000000 --- a/src/vs/workbench/api/test/browser/extHostNotebookConcatDocument.test.ts +++ /dev/null @@ -1,620 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { TestRPCProtocol } from 'vs/workbench/api/test/common/testRPCProtocol'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; -import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { ExtHostNotebookDocument } from 'vs/workbench/api/common/extHostNotebookDocument'; -import { URI } from 'vs/base/common/uri'; -import { CellKind, CellUri, NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Position, Location, Range } from 'vs/workbench/api/common/extHostTypes'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; -import * as vscode from 'vscode'; -import { mock } from 'vs/workbench/test/common/workbenchTestServices'; -import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; -import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import { generateUuid } from 'vs/base/common/uuid'; -import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; -import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; - -suite('NotebookConcatDocument', function () { - - let rpcProtocol: TestRPCProtocol; - let notebook: ExtHostNotebookDocument; - let extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; - let extHostDocuments: ExtHostDocuments; - let extHostNotebooks: ExtHostNotebookController; - let extHostNotebookDocuments: ExtHostNotebookDocuments; - - const notebookUri = URI.parse('test:///notebook.file'); - const disposables = new DisposableStore(); - - setup(async function () { - disposables.clear(); - - rpcProtocol = new TestRPCProtocol(); - rpcProtocol.set(MainContext.MainThreadCommands, new class extends mock() { - override $registerCommand() { } - }); - rpcProtocol.set(MainContext.MainThreadNotebook, new class extends mock() { - override async $registerNotebookProvider() { } - override async $unregisterNotebookProvider() { } - }); - extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - const extHostStoragePaths = new class extends mock() { - override workspaceValue() { - return URI.from({ scheme: 'test', path: generateUuid() }); - } - }; - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, extHostDocuments, extHostStoragePaths); - extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); - - let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { - // async openNotebook() { } - }); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ - addedDocuments: [{ - uri: notebookUri, - viewType: 'test', - cells: [{ - handle: 0, - uri: CellUri.generate(notebookUri, 0), - source: ['### Heading'], - eol: '\n', - language: 'markdown', - cellKind: CellKind.Markup, - outputs: [], - }], - versionId: 0 - }], - addedEditors: [{ - documentUri: notebookUri, - id: '_notebook_editor_0', - selections: [{ start: 0, end: 1 }], - visibleRanges: [] - }] - })); - extHostNotebooks.$acceptDocumentAndEditorsDelta(new SerializableObjectWithBuffers({ newActiveEditor: '_notebook_editor_0' })); - - notebook = extHostNotebooks.notebookDocuments[0]!; - - disposables.add(reg); - disposables.add(notebook); - disposables.add(extHostDocuments); - }); - - test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assert.strictEqual(doc.getText(), ''); - assert.strictEqual(doc.version, 0); - - // assert.strictEqual(doc.locationAt(new Position(0, 0)), undefined); - // assert.strictEqual(doc.positionAt(SOME_FAKE_LOCATION?), undefined); - }); - - - function assertLocation(doc: vscode.NotebookConcatTextDocument, pos: Position, expected: Location, reverse = true) { - const actual = doc.locationAt(pos); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.isEqual(expected.range), true); - - if (reverse) { - // reverse - offset - const offset = doc.offsetAt(pos); - assert.strictEqual(doc.positionAt(offset).isEqual(pos), true); - - // reverse - pos - const actualPosition = doc.positionAt(actual); - assert.strictEqual(actualPosition.isEqual(pos), true); - } - } - - function assertLines(doc: vscode.NotebookConcatTextDocument, ...lines: string[]) { - let actual = doc.getText().split(/\r\n|\n|\r/); - assert.deepStrictEqual(actual, lines); - } - - test('contains', function () { - - const cellUri1 = CellUri.generate(notebook.uri, 1); - const cellUri2 = CellUri.generate(notebook.uri, 2); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [{ - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: cellUri1, - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: cellUri2, - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]] - ] - }] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - assert.strictEqual(doc.contains(cellUri1), true); - assert.strictEqual(doc.contains(cellUri2), true); - assert.strictEqual(doc.contains(URI.parse('some://miss/path')), false); - }); - - test('location, position mapping', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - }); - - - test('location, position mapping, cell changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 1); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - - - // UPDATE 2 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 0, [{ - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 2); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(2, 11)), false); // don't check identity because position will be clamped - - // UPDATE 3 (remove cell #2 again) - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[1, 1, []]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 1); - assert.strictEqual(doc.version, 3); - assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12)), false); // clamped - }); - - test('location, position mapping, cell-document changes', function () { - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - - // UPDATE 1 - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); - assert.strictEqual(doc.version, 1); - - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.apiNotebook.cellAt(1).document.uri, new Position(1, 3))); - - // offset math - let cell1End = doc.offsetAt(new Position(2, 12)); - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - - extHostDocuments.$acceptModelChanged(notebook.apiNotebook.cellAt(0).document.uri, { - versionId: 0, - eol: '\n', - changes: [{ - range: { startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 6 }, - rangeLength: 6, - rangeOffset: 12, - text: 'Hi' - }], - isRedoing: false, - isUndoing: false, - }, false); - assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.apiNotebook.cellAt(0).document.uri, new Position(2, 9)), false); - - assert.strictEqual(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); - - }); - - test('selector', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['fooLang-document'], - eol: '\n', - language: 'fooLang', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['barLang-document'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, 'barLang'); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document'); - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[2, 0, [{ - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['barLang-document2'], - eol: '\n', - language: 'barLang', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); - assertLines(fooLangDoc, 'fooLang-document'); - assertLines(barLangDoc, 'barLang-document', 'barLang-document2'); - }); - - function assertOffsetAtPosition(doc: vscode.NotebookConcatTextDocument, offset: number, expected: { line: number; character: number }, reverse = true) { - const actual = doc.positionAt(offset); - - assert.strictEqual(actual.line, expected.line); - assert.strictEqual(actual.character, expected.character); - - if (reverse) { - const actualOffset = doc.offsetAt(actual); - assert.strictEqual(actualOffset, offset); - } - } - - - test('offsetAt(position) <-> positionAt(offset)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); - assertOffsetAtPosition(doc, 1, { line: 0, character: 1 }); - assertOffsetAtPosition(doc, 9, { line: 1, character: 3 }); - assertOffsetAtPosition(doc, 32, { line: 4, character: 1 }); - assertOffsetAtPosition(doc, 47, { line: 5, character: 11 }); - }); - - - function assertLocationAtPosition(doc: vscode.NotebookConcatTextDocument, pos: { line: number; character: number }, expected: { uri: URI; line: number; character: number }, reverse = true) { - - const actual = doc.locationAt(new Position(pos.line, pos.character)); - assert.strictEqual(actual.uri.toString(), expected.uri.toString()); - assert.strictEqual(actual.range.start.line, expected.line); - assert.strictEqual(actual.range.end.line, expected.line); - assert.strictEqual(actual.range.start.character, expected.character); - assert.strictEqual(actual.range.end.character, expected.character); - - if (reverse) { - const actualPos = doc.positionAt(actual); - assert.strictEqual(actualPos.line, pos.line); - assert.strictEqual(actualPos.character, pos.character); - } - } - - test('locationAt(position) <-> positionAt(location)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.apiNotebook.cellAt(0).document.uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.apiNotebook.cellAt(1).document.uri, line: 2, character: 11 }); - }); - - test('getText(range)', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 3, - uri: CellUri.generate(notebook.uri, 3), - source: ['Three', 'Drei', 'Drüü'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 3); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!', 'Three', 'Drei', 'Drüü'); - - assert.strictEqual(doc.getText(new Range(0, 0, 0, 0)), ''); - assert.strictEqual(doc.getText(new Range(0, 0, 1, 0)), 'Hello\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 4, 0)), 'Hello World!\nHallo\n'); - assert.strictEqual(doc.getText(new Range(2, 0, 8, 0)), 'Hello World!\nHallo\nWelt\nHallo Welt!\nThree\nDrei\n'); - }); - - test('validateRange/Position', function () { - - extHostNotebookDocuments.$acceptModelChanged(notebookUri, new SerializableObjectWithBuffers({ - versionId: notebook.apiNotebook.version + 1, - rawEvents: [ - { - kind: NotebookCellsChangeType.ModelChange, - changes: [[0, 0, [{ - handle: 1, - uri: CellUri.generate(notebook.uri, 1), - source: ['Hello', 'World', 'Hello World!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }, { - handle: 2, - uri: CellUri.generate(notebook.uri, 2), - source: ['Hallo', 'Welt', 'Hallo Welt!'], - eol: '\n', - language: 'test', - cellKind: CellKind.Code, - outputs: [], - }]]] - } - ] - }), false); - - assert.strictEqual(notebook.apiNotebook.cellCount, 1 + 2); // markdown and code - - let doc = new ExtHostNotebookConcatDocument(extHostNotebookDocuments, extHostDocuments, notebook.apiNotebook, undefined); - assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - - - function assertPosition(actual: vscode.Position, expectedLine: number, expectedCh: number) { - assert.strictEqual(actual.line, expectedLine); - assert.strictEqual(actual.character, expectedCh); - } - - - // "fixed" - assertPosition(doc.validatePosition(new Position(0, 1000)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 1000)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 1000)), 5, 11); - assertPosition(doc.validatePosition(new Position(5000, 1000)), 5, 11); - - // "good" - assertPosition(doc.validatePosition(new Position(0, 1)), 0, 1); - assertPosition(doc.validatePosition(new Position(0, 5)), 0, 5); - assertPosition(doc.validatePosition(new Position(2, 8)), 2, 8); - assertPosition(doc.validatePosition(new Position(2, 12)), 2, 12); - assertPosition(doc.validatePosition(new Position(5, 11)), 5, 11); - - }); -}); diff --git a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts index 20ef8c11b6c..fb2b02bc890 100644 --- a/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts +++ b/src/vs/workbench/api/test/browser/extHostTypeConverter.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MarkdownString, NotebookCellOutputItem, NotebookData } from 'vs/workbench/api/common/extHostTypeConverters'; +import { MarkdownString, NotebookCellOutputItem, NotebookData, LanguageSelector } from 'vs/workbench/api/common/extHostTypeConverters'; import { isEmptyObject } from 'vs/base/common/types'; import { forEach } from 'vs/base/common/collections'; import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; @@ -111,4 +111,16 @@ suite('ExtHostTypeConverter', function () { assert.strictEqual(item2.mime, item.mime); assert.deepStrictEqual(Array.from(item2.data), Array.from(item.data)); }); + + test('LanguageSelector', function () { + const out = LanguageSelector.from({ language: 'bat', notebookType: 'xxx' }); + assert.ok(typeof out === 'object'); + assert.deepStrictEqual(out, { + language: 'bat', + notebookType: 'xxx', + scheme: undefined, + pattern: undefined, + exclusive: undefined, + }); + }); }); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 68c7c834910..a74fcfbc69b 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -44,7 +44,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { // initialize API and register actors const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors); - this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry); + this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, { mine: this._myRegistry, all: this._globalRegistry }); await this._fakeModules.install(); performance.mark('code/extHost/didInitAPI'); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index aaf84ac8ef7..8440c0f8e12 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -22,7 +22,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions'; import { TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext } from 'vs/workbench/common/contextkeys'; +import { AuxiliaryBarVisibleContext, PanelAlignmentContext, PanelVisibleContext, SideBarVisibleContext, FocusedViewContext, InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext, IsFullscreenContext, PanelPositionContext } from 'vs/workbench/common/contextkeys'; import { Codicon } from 'vs/base/common/codicons'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; @@ -330,12 +330,8 @@ registerAction2(class extends Action2 { category: CATEGORIES.View, f1: true, toggled: EditorAreaVisibleContext, - // Remove from appearance menu - // menu: [{ - // id: MenuId.MenubarAppearanceMenu, - // group: '2_workbench_layout', - // order: 5 - // }] + // the workbench grid currently prevents us from supporting panel maximization with non-center panel alignment + precondition: ContextKeyExpr.or(PanelAlignmentContext.isEqualTo('center'), PanelPositionContext.notEqualsTo('bottom')) }); } @@ -505,13 +501,7 @@ registerAction2(class extends Action2 { original: 'Toggle Tab Visibility' }, category: CATEGORIES.View, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: undefined, - mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, }, - linux: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyW, } - } + f1: true }); } diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 748ababc6db..a0d14adf991 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -145,6 +145,7 @@ abstract class BaseOpenRecentAction extends Action2 { matchOnDescription: true, onKeyMods: mods => keyMods = mods, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { // Remove diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index e0b778dd82f..4cfe9f32bb5 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -272,14 +272,15 @@ class ResourceLabelWidget extends IconLabel { private readonly _onDidRender = this._register(new Emitter()); readonly onDidRender = this._onDidRender.event; - private label?: IResourceLabelProps; + private label: IResourceLabelProps | undefined = undefined; private decoration = this._register(new MutableDisposable()); - private options?: IResourceLabelOptions; - private computedIconClasses?: string[]; - private lastKnownDetectedLanguageId?: string; - private computedPathLabel?: string; + private options: IResourceLabelOptions | undefined = undefined; - private needsRedraw?: Redraw; + private computedIconClasses: string[] | undefined = undefined; + private computedLanguageId: string | undefined = undefined; + private computedPathLabel: string | undefined = undefined; + + private needsRedraw: Redraw | undefined = undefined; private isHidden: boolean = false; constructor( @@ -325,8 +326,8 @@ class ResourceLabelWidget extends IconLabel { } if (isEqual(model.uri, resource)) { - if (this.lastKnownDetectedLanguageId !== model.getLanguageId()) { - this.lastKnownDetectedLanguageId = model.getLanguageId(); + if (this.computedLanguageId !== model.getLanguageId()) { + this.computedLanguageId = model.getLanguageId(); this.render({ updateIcon: true, updateDecoration: false }); // update if the language id of the model has changed from our last known state } } @@ -444,8 +445,12 @@ class ResourceLabelWidget extends IconLabel { this.label = label; this.options = options; + if (hasResourceChanged) { + this.computedLanguageId = undefined; // reset computed language since resource changed + } + if (hasPathLabelChanged) { - this.computedPathLabel = undefined; // reset path label due to resource change + this.computedPathLabel = undefined; // reset path label due to resource/path-label change } this.render({ @@ -485,7 +490,7 @@ class ResourceLabelWidget extends IconLabel { clear(): void { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; @@ -594,7 +599,7 @@ class ResourceLabelWidget extends IconLabel { this.label = undefined; this.options = undefined; - this.lastKnownDetectedLanguageId = undefined; + this.computedLanguageId = undefined; this.computedIconClasses = undefined; this.computedPathLabel = undefined; } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 1bf6c4750e2..1309b3de8e1 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -617,16 +617,22 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return { filesToOpenOrCreate: defaultLayout.editors.map(file => { + const legacyOverride = file.openWith; + const legacySelection = file.selection && file.selection.start && isNumber(file.selection.start.line) ? { + startLineNumber: file.selection.start.line, + startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, + endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, + endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, + } : undefined; + return { fileUri: URI.revive(file.uri), - selection: file.selection && file.selection.start && isNumber(file.selection.start.line) ? { - startLineNumber: file.selection.start.line, - startColumn: isNumber(file.selection.start.column) ? file.selection.start.column : 1, - endLineNumber: isNumber(file.selection.end.line) ? file.selection.end.line : undefined, - endColumn: isNumber(file.selection.end.line) ? (isNumber(file.selection.end.column) ? file.selection.end.column : 1) : undefined, - } : undefined, openOnlyIfExists: file.openOnlyIfExists, - editorOverrideId: file.openWith + options: { + selection: legacySelection, + override: legacyOverride, + ...file.options // keep at the end to override legacy selection/override that may be `undefined` + } }; }) }; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 22e337fd491..de6e99067ef 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -427,12 +427,6 @@ export class CompositeBar extends Widget implements ICompositeBar { this.options.openComposite(defaultCompositeId, true); } - // Case: we closed the last visible composite - // Solv: we hide the part - else if (this.visibleComposites.length <= 1) { - this.options.hidePart(); - } - // Case: we closed the default composite // Solv: we open the next visible composite from top else { diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 0995d4e6e48..a993829e847 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -19,7 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; -import { IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; +import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { ItemActivation, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; import { Codicon } from 'vs/base/common/codicons'; @@ -1483,13 +1483,26 @@ export class ClearRecentFilesAction extends Action { id: string, label: string, @IWorkspacesService private readonly workspacesService: IWorkspacesService, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise { + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearRecentsMessage', "Do you want to clear all recently opened files and workspaces?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + // Clear global recently opened this.workspacesService.clearRecentlyOpened(); @@ -1746,14 +1759,27 @@ export class ClearEditorHistoryAction extends Action { constructor( id: string, label: string, - @IHistoryService private readonly historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IDialogService private readonly dialogService: IDialogService ) { super(id, label); } override async run(): Promise { - // Editor history + // Ask for confirmation + const { confirmed } = await this.dialogService.confirm({ + message: localize('confirmClearEditorHistoryMessage', "Do you want to clear the history of recently opened editors?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + + // Clear editor history this.historyService.clear(); } } diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index e3d008f61cc..0ee0ad2f093 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -4,37 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editordroptarget'; -import { localize } from 'vs/nls'; -import { Extensions as DragAndDropExtensions, LocalSelectionTransfer, DraggedEditorIdentifier, ResourcesDropHandler, DraggedEditorGroupIdentifier, containsDragType, CodeDataTransfers, DraggedTreeItemsIdentifier, extractTreeDropData, IDragAndDropContributionRegistry } from 'vs/workbench/browser/dnd'; -import { addDisposableListener, EventType, EventHelper, isAncestor, DragAndDropObserver } from 'vs/base/browser/dom'; -import { IEditorGroupsAccessor, IEditorGroupView, fillActiveEditorViewState } from 'vs/workbench/browser/parts/editor/editor'; -import { EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; -import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { IEditorIdentifier, EditorInputCapabilities, IUntypedEditorInput } from 'vs/workbench/common/editor'; -import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { toDisposable } from 'vs/base/common/lifecycle'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { RunOnceScheduler } from 'vs/base/common/async'; import { DataTransfers } from 'vs/base/browser/dnd'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; -import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; -import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { addDisposableListener, DragAndDropObserver, EventHelper, EventType, isAncestor } from 'vs/base/browser/dom'; +import { renderFormattedText } from 'vs/base/browser/formattedTextRenderer'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; +import { assertAllDefined, assertIsDefined } from 'vs/base/common/types'; +import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; +import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; +import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { CodeDataTransfers, containsDragType, DraggedEditorGroupIdentifier, DraggedEditorIdentifier, DraggedTreeItemsIdentifier, Extensions as DragAndDropExtensions, extractTreeDropData, IDragAndDropContributionRegistry, LocalSelectionTransfer, ResourcesDropHandler } from 'vs/workbench/browser/dnd'; +import { fillActiveEditorViewState, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { EditorInputCapabilities, IEditorIdentifier, IUntypedEditorInput } from 'vs/workbench/common/editor'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BACKGROUND, EDITOR_DROP_INTO_PROMPT_BORDER, EDITOR_DROP_INTO_PROMPT_FOREGROUND } from 'vs/workbench/common/theme'; +import { GroupDirection, IEditorGroupsService, IMergeGroupOptions, MergeGroupMode } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ITreeViewsService } from 'vs/workbench/services/views/browser/treeViewsService'; interface IDropOperation { splitDirection?: GroupDirection; } -function isDropIntoEditorEnabled(configurationService: IConfigurationService) { - return configurationService.getValue('workbench.editor.dropIntoEditor.enabled'); +function isDropIntoEditorEnabledGlobally(configurationService: IConfigurationService) { + return configurationService.getValue('workbench.experimental.editor.dropIntoEditor.enabled'); } -function isDragIntoEditorEvent(configurationService: IConfigurationService, e: DragEvent): boolean { - return isDropIntoEditorEnabled(configurationService) && e.shiftKey; +function isDragIntoEditorEvent(e: DragEvent): boolean { + return e.shiftKey; } class DropOverlay extends Themable { @@ -46,7 +47,9 @@ class DropOverlay extends Themable { private dropIntoPromptElement?: HTMLSpanElement; private currentDropOperation: IDropOperation | undefined; + private _disposed: boolean | undefined; + get disposed(): boolean { return !!this._disposed; } private cleanupOverlayScheduler: RunOnceScheduler; @@ -54,6 +57,8 @@ class DropOverlay extends Themable { private readonly groupTransfer = LocalSelectionTransfer.getInstance(); private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance(); + private readonly enableDropIntoEditor: boolean; + constructor( private accessor: IEditorGroupsAccessor, private groupView: IEditorGroupView, @@ -69,11 +74,9 @@ class DropOverlay extends Themable { this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); - this.create(); - } + this.enableDropIntoEditor = isDropIntoEditorEnabledGlobally(this.configurationService) && this.isDropIntoActiveEditorEnabled(); - get disposed(): boolean { - return !!this._disposed; + this.create(); } private create(): void { @@ -97,10 +100,9 @@ class DropOverlay extends Themable { this.overlay.classList.add('editor-group-overlay-indicator'); container.appendChild(this.overlay); - if (isDropIntoEditorEnabled(this.configurationService)) { - this.dropIntoPromptElement = document.createElement('span'); + if (this.enableDropIntoEditor) { + this.dropIntoPromptElement = renderFormattedText(localize('dropIntoEditorPrompt', "Hold __{0}__ to drop into editor", isMacintosh ? '⇧' : 'Shift'), {}); this.dropIntoPromptElement.classList.add('editor-group-overlay-drop-into-prompt'); - this.dropIntoPromptElement.textContent = localize('dropIntoEditorPrompt', "Hold shift to drop into editor"); this.overlay.appendChild(this.dropIntoPromptElement); } @@ -123,13 +125,27 @@ class DropOverlay extends Themable { overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; + + if (this.dropIntoPromptElement) { + this.dropIntoPromptElement.style.backgroundColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BACKGROUND) ?? ''; + this.dropIntoPromptElement.style.color = this.getColor(EDITOR_DROP_INTO_PROMPT_FOREGROUND) ?? ''; + + const borderColor = this.getColor(EDITOR_DROP_INTO_PROMPT_BORDER); + if (borderColor) { + this.dropIntoPromptElement.style.borderWidth = '1px'; + this.dropIntoPromptElement.style.borderStyle = 'solid'; + this.dropIntoPromptElement.style.borderColor = borderColor; + } else { + this.dropIntoPromptElement.style.borderWidth = '0'; + } + } } private registerListeners(container: HTMLElement): void { this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { - if (isDragIntoEditorEvent(this.configurationService, e)) { + if (this.enableDropIntoEditor && isDragIntoEditorEvent(e)) { this.dispose(); return; } @@ -209,6 +225,10 @@ class DropOverlay extends Themable { })); } + private isDropIntoActiveEditorEnabled(): boolean { + return !!this.groupView.activeEditor?.hasCapability(EditorInputCapabilities.CanDropIntoEditor); + } + private findSourceGroupView(): IEditorGroupView | undefined { // Check for group transfer @@ -588,7 +608,7 @@ export class EditorDropTarget extends Themable { } private onDragEnter(event: DragEvent): void { - if (isDragIntoEditorEvent(this.configurationService, event)) { + if (isDropIntoEditorEnabledGlobally(this.configurationService) && isDragIntoEditorEvent(event)) { return; } diff --git a/src/vs/workbench/browser/parts/editor/editorPanes.ts b/src/vs/workbench/browser/parts/editor/editorPanes.ts index 9c47fb1d941..268c214a232 100644 --- a/src/vs/workbench/browser/parts/editor/editorPanes.ts +++ b/src/vs/workbench/browser/parts/editor/editorPanes.ts @@ -158,7 +158,7 @@ export class EditorPanes extends Disposable { } const buttons: string[] = []; - if (Array.isArray(errorActions) && errorActions.length > 0) { + if (errorActions && errorActions.length > 0) { for (const errorAction of errorActions) { buttons.push(errorAction.label); } @@ -183,10 +183,14 @@ export class EditorPanes extends Disposable { ); // Make sure to run any error action if present - if (result.choice !== cancelId && Array.isArray(errorActions)) { + if (result.choice !== cancelId && errorActions) { const errorAction = errorActions[result.choice]; if (errorAction) { - errorAction.run(); + const result = errorAction.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + errorHandled = true; // consider the error as handled! } } diff --git a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts index 5e269c439a1..0464f14265b 100644 --- a/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts +++ b/src/vs/workbench/browser/parts/editor/editorPlaceholder.ts @@ -5,6 +5,7 @@ import 'vs/css!./media/editorplaceholder'; import { localize } from 'vs/nls'; +import Severity from 'vs/base/common/severity'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -28,6 +29,7 @@ import { editorErrorForeground, editorInfoForeground, editorWarningForeground } import { Codicon } from 'vs/base/common/codicons'; import { FileChangeType, FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { isErrorWithActions, toErrorMessage } from 'vs/base/common/errorMessage'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; export interface IEditorPlaceholderContents { icon: string; @@ -214,7 +216,8 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IInstantiationService instantiationService: IInstantiationService, - @IFileService private readonly fileService: IFileService + @IFileService private readonly fileService: IFileService, + @IDialogService private readonly dialogService: IDialogService ) { super(ErrorPlaceholderEditor.ID, telemetryService, themeService, storageService, instantiationService); } @@ -241,7 +244,12 @@ export class ErrorPlaceholderEditor extends EditorPlaceholder { actions = error.actions.map(action => { return { label: action.label, - run: () => action.run() + run: () => { + const result = action.run(); + if (result instanceof Promise) { + result.catch(error => this.dialogService.show(Severity.Error, toErrorMessage(error))); + } + } }; }); } else if (group) { diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 92091e6054d..7571481ae97 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -1202,7 +1202,7 @@ export class ChangeLanguageAction extends Action { if (resource) { // Detect languages since we are in an untitled file let languageId: string | undefined = withNullAsUndefined(this.languageService.guessLanguageIdByFilepathOrFirstLine(resource, textModel.getLineContent(1))); - if (!languageId) { + if (!languageId || languageId === 'unknown') { detectedLanguage = await this.languageDetectionService.detectLanguage(resource); languageId = detectedLanguage; } diff --git a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css index 812d42d7611..8a50b279785 100644 --- a/src/vs/workbench/browser/parts/editor/media/editordroptarget.css +++ b/src/vs/workbench/browser/parts/editor/media/editordroptarget.css @@ -28,11 +28,22 @@ #monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt { text-align: center; - padding: 1em; + padding: 0.6em; + margin: 0.2em; + line-height: normal; opacity: 0; /* hidden initially */ transition: opacity 150ms ease-out; } +#monaco-workbench-editor-drop-overlay .editor-group-overlay-drop-into-prompt i /* Style keybinding */ { + padding: 0 8px; + border: 1px solid hsla(0,0%,80%,.4); + margin: 0 1px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 0.05); + font-style: normal; +} + #monaco-workbench-editor-drop-overlay > .editor-group-overlay-indicator.overlay-move-transition { transition: top 70ms ease-out, left 70ms ease-out, width 70ms ease-out, height 70ms ease-out, opacity 150ms ease-out; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index b0a1cfb5253..f1d36bc3147 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -370,6 +370,14 @@ export abstract class BasePanelPart extends CompositePart impleme if (viewContainerModel.activeViewDescriptors.length) { contextKey.set(true); this.compositeBar.addComposite({ id: viewContainer.id, name: viewContainer.title, order: viewContainer.order, requestedIndex: viewContainer.requestedIndex }); + + const activeComposite = this.getActiveComposite(); + if (activeComposite === undefined || activeComposite.getId() === viewContainer.id) { + this.compositeBar.activateComposite(viewContainer.id); + } + + this.layoutCompositeBar(); + this.layoutEmptyMessage(); } else if (viewContainer.hideIfEmpty) { contextKey.set(false); this.hideComposite(viewContainer.id); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4934df5077c..52771febbf5 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -12,7 +12,7 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; @@ -411,9 +411,16 @@ export class TitlebarPart extends Part implements ITitleService { this.layoutToolbar = new ToolBar(layoutDropdownContainer, this.contextMenuService, { actionViewItemProvider: action => { return createActionViewItem(this.instantiationService, action); - } + }, + allowContextMenu: true }); + this._register(addDisposableListener(layoutDropdownContainer, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e); + + this.onLayoutControlContextMenu(e, layoutDropdownContainer); + })); + const menu = this._register(this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService)); const updateLayoutMenu = () => { @@ -507,7 +514,6 @@ export class TitlebarPart extends Part implements ITitleService { } private onContextMenu(e: MouseEvent): void { - // Find target anchor const event = new StandardMouseEvent(e); const anchor = { x: event.posx, y: event.posy }; @@ -524,6 +530,28 @@ export class TitlebarPart extends Part implements ITitleService { }); } + private onLayoutControlContextMenu(e: MouseEvent, el: HTMLElement): void { + // Find target anchor + const event = new StandardMouseEvent(e); + const anchor = { x: event.posx, y: event.posy }; + + const actions: IAction[] = []; + actions.push(toAction({ + id: 'layoutControl.hide', + label: localize('layoutControl.hide', "Hide Layout Control"), + run: () => { + this.configurationService.updateValue('workbench.layoutControl.enabled', false); + } + })); + + // Show it + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + domForShadowRoot: el + }); + } + protected adjustTitleMarginToCenter(): void { if (this.customMenubar && this.menubar) { const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; diff --git a/src/vs/workbench/browser/web.api.ts b/src/vs/workbench/browser/web.api.ts index 644e3c3f3af..effacb4a75f 100644 --- a/src/vs/workbench/browser/web.api.ts +++ b/src/vs/workbench/browser/web.api.ts @@ -15,6 +15,9 @@ import type { IProductConfiguration } from 'vs/base/common/product'; import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; import type { TunnelProviderFeatures } from 'vs/platform/tunnel/common/tunnel'; import type { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; +import { IEditorOptions } from 'vs/platform/editor/common/editor'; /** * The `IWorkbench` interface is the API facade for web embedders @@ -42,7 +45,7 @@ export interface IWorkbench { * @returns the scheme to use for opening the associated desktop * experience via protocol handler. */ - readonly uriScheme: string; + getUriScheme(): Promise; /** * Retrieve performance marks that have been collected during startup. This function @@ -62,6 +65,11 @@ export interface IWorkbench { * workbench. */ openUri(target: URI): Promise; + + /** + * Current workbench telemetry level. + */ + readonly telemetryLevel: IObservableValue; }; window: { @@ -553,32 +561,42 @@ export interface IDefaultView { readonly id: string; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IPosition { readonly line: number; readonly column: number; } +/** + * @deprecated use `IDefaultEditor.options` instead + */ export interface IRange { - - /** - * The start position. It is before or equal to end position. - */ readonly start: IPosition; - - /** - * The end position. It is after or equal to start position. - */ readonly end: IPosition; } export interface IDefaultEditor { + readonly uri: UriComponents; - readonly selection?: IRange; + readonly options?: IEditorOptions; + readonly openOnlyIfExists?: boolean; + + /** + * @deprecated use `options` instead + */ + readonly selection?: IRange; + + /** + * @deprecated use `options.override` instead + */ readonly openWith?: string; } export interface IDefaultLayout { + readonly views?: IDefaultView[]; readonly editors?: IDefaultEditor[]; @@ -642,4 +660,3 @@ export interface IDevelopmentOptions { */ readonly enableSmokeTestDriver?: boolean; } - diff --git a/src/vs/workbench/browser/web.factory.ts b/src/vs/workbench/browser/web.factory.ts index 8eb57787353..49c7eb489e9 100644 --- a/src/vs/workbench/browser/web.factory.ts +++ b/src/vs/workbench/browser/web.factory.ts @@ -13,6 +13,8 @@ import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { DeferredPromise } from 'vs/base/common/async'; import { asArray } from 'vs/base/common/arrays'; import { IProgress, IProgressCompositeOptions, IProgressDialogOptions, IProgressNotificationOptions, IProgressOptions, IProgressStep, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; +import { IObservableValue } from 'vs/base/common/observableValue'; +import { TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; let created = false; const workbenchPromise = new DeferredPromise(); @@ -109,7 +111,7 @@ export namespace env { export async function getUriScheme(): Promise { const workbench = await workbenchPromise.p; - return workbench.env.uriScheme; + return workbench.env.getUriScheme(); } /** @@ -120,6 +122,9 @@ export namespace env { return workbench.env.openUri(target); } + + export const telemetryLevel: Promise> = + workbenchPromise.p.then(workbench => workbench.env.telemetryLevel); } export namespace window { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index c173f780471..e8ca913a532 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -69,6 +69,7 @@ import { IndexedDB } from 'vs/base/browser/indexedDB'; import { BrowserCredentialsService } from 'vs/workbench/services/credentials/browser/credentialsService'; import { IWorkspace } from 'vs/workbench/services/host/browser/browserHostService'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IProgressService } from 'vs/platform/progress/common/progress'; export class BrowserMain extends Disposable { @@ -117,6 +118,7 @@ export class BrowserMain extends Disposable { const timerService = accessor.get(ITimerService); const openerService = accessor.get(IOpenerService); const productService = accessor.get(IProductService); + const telemetryService = accessor.get(ITelemetryService); const progessService = accessor.get(IProgressService); return { @@ -124,7 +126,10 @@ export class BrowserMain extends Disposable { executeCommand: (command, ...args) => commandService.executeCommand(command, ...args) }, env: { - uriScheme: productService.urlProtocol, + telemetryLevel: telemetryService.telemetryLevel, + async getUriScheme(): Promise { + return productService.urlProtocol; + }, async retrievePerformanceMarks() { await timerService.whenReady(); @@ -248,7 +253,7 @@ export class BrowserMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/browser/window.ts b/src/vs/workbench/browser/window.ts index 1edef45d307..229027ab56e 100644 --- a/src/vs/workbench/browser/window.ts +++ b/src/vs/workbench/browser/window.ts @@ -65,9 +65,9 @@ export class BrowserWindow extends Disposable { this._register(addDisposableListener(this.layoutService.container, EventType.DROP, e => EventHelper.stop(e, true))); // Fullscreen (Browser) - [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE].forEach(event => { + for (const event of [EventType.FULLSCREEN_CHANGE, EventType.WK_FULLSCREEN_CHANGE]) { this._register(addDisposableListener(document, event, () => setFullscreen(!!detectFullscreen()))); - }); + } // Fullscreen (Native) this._register(addDisposableThrottledListener(viewport, EventType.RESIZE, () => { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 46d04a0b0ec..903ca298a14 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -111,6 +111,19 @@ const registry = Registry.as(ConfigurationExtensions.Con tags: ['experimental'], description: localize('workbench.editor.preferBasedLanguageDetection', "When enabled, a language detection model that takes into account editor history will be given higher precedence."), }, + 'workbench.editor.languageDetectionHints': { + type: 'string', + default: 'always', + tags: ['experimental'], + enum: ['always', 'notebookEditors', 'textEditors', 'never'], + description: localize('workbench.editor.showLanguageDetectionHints', "When enabled, shows a status bar quick fix when the editor language doesn't match detected content language."), + enumDescriptions: [ + localize('workbench.editor.showLanguageDetectionHints.always', "Show show language detection quick fixes in both notebooks and untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.notebook', "Only show language detection quick fixes in notebooks"), + localize('workbench.editor.showLanguageDetectionHints.editors', "Only show language detection quick fixes in untitled editors"), + localize('workbench.editor.showLanguageDetectionHints.never', "Never show language quick fixes"), + ] + }, 'workbench.editor.tabCloseButton': { 'type': 'string', 'enum': ['left', 'right', 'off'], @@ -464,9 +477,10 @@ const registry = Registry.as(ConfigurationExtensions.Con 'description': localize('layoutControlType', "Controls whether the layout control in the custom title bar is displayed as a single menu button or with multiple UI toggles."), 'markdownDeprecationMessage': localize({ key: 'layoutControlTypeDeprecation', comment: ['{0} is a placeholder for a setting identifier.'] }, "This setting has been deprecated in favor of {0}", '`#workbench.layoutControl.type#`') }, - 'workbench.editor.dropIntoEditor.enabled': { + 'workbench.experimental.editor.dropIntoEditor.enabled': { 'type': 'boolean', 'default': true, + 'tags': ['experimental'], 'markdownDescription': localize('dropIntoEditor', "Controls whether you can drag and drop a file into a text editor by holding down `shift` (instead of opening the file in an editor)."), } } diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index f2c5bf51ade..27c50770822 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -336,7 +336,7 @@ export class Workbench extends Layout { this.restoreFontInfo(storageService, configurationService); // Create Parts - [ + for (const { id, role, classes, options } of [ { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 @@ -345,11 +345,11 @@ export class Workbench extends Layout { { id: Parts.PANEL_PART, role: 'none', classes: ['panel', 'basepanel', positionToString(this.getPanelPosition())] }, { id: Parts.AUXILIARYBAR_PART, role: 'none', classes: ['auxiliarybar', 'basepanel', this.getSideBarPosition() === Position.LEFT ? 'right' : 'left'] }, { id: Parts.STATUSBAR_PART, role: 'status', classes: ['statusbar'] } - ].forEach(({ id, role, classes, options }) => { + ]) { const partContainer = this.createPart(id, role, classes); this.getPart(id).create(partContainer, options); - }); + } // Notification Handlers this.createNotificationsHandlers(instantiationService, notificationService); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index bd610ebd12d..b12e6477782 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -9,7 +9,7 @@ import { assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditorViewState, IDiffEditor, IDiffEditorViewState, IEditorViewState } from 'vs/editor/common/editorCommon'; -import { IEditorOptions, ITextEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceEditorInput, ITextResourceEditorInput, IBaseTextResourceEditorInput, IBaseUntypedEditorInput } from 'vs/platform/editor/common/editor'; import type { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { IInstantiationService, IConstructorSignature, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -675,7 +675,13 @@ export const enum EditorInputCapabilities { * component may decide to hide the description portion * for brevity. */ - ForceDescription = 1 << 6 + ForceDescription = 1 << 6, + + /** + * Signals that the editor supports dropping into the + * editor by holding shift. + */ + CanDropIntoEditor = 1 << 7, } export type IUntypedEditorInput = IResourceEditorInput | ITextResourceEditorInput | IUntitledTextResourceEditorInput | IResourceDiffEditorInput | IResourceSideBySideEditorInput; @@ -1334,10 +1340,9 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService return; } - const options: ITextEditorOptions = { - selection: exists ? path.selection : undefined, - pinned: true, - override: path.editorOverrideId + const options: IEditorOptions = { + ...path.options, + pinned: true }; let input: IResourceEditorInput | IUntitledTextResourceEditorInput; diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 062f9a50439..190a6e63db5 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -26,6 +26,10 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements capabilities |= EditorInputCapabilities.Untitled; } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index fb7dcc2c042..efd9c628e99 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -279,6 +279,27 @@ export const EDITOR_DRAG_AND_DROP_BACKGROUND = registerColor('editorGroup.dropBa hcLight: Color.fromHex('#0F4A85').transparent(0.50) }, localize('editorDragAndDropBackground', "Background color when dragging editors around. The color should have transparency so that the editor contents can still shine through.")); +export const EDITOR_DROP_INTO_PROMPT_FOREGROUND = registerColor('editorGroup.dropIntoPromptForeground', { + dark: editorWidgetForeground, + light: editorWidgetForeground, + hcDark: editorWidgetForeground, + hcLight: editorWidgetForeground +}, localize('editorDropIntoPromptForeground', "Foreground color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BACKGROUND = registerColor('editorGroup.dropIntoPromptBackground', { + dark: editorWidgetBackground, + light: editorWidgetBackground, + hcDark: editorWidgetBackground, + hcLight: editorWidgetBackground +}, localize('editorDropIntoPromptBackground', "Background color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + +export const EDITOR_DROP_INTO_PROMPT_BORDER = registerColor('editorGroup.dropIntoPromptBorder', { + dark: null, + light: null, + hcDark: contrastBorder, + hcLight: contrastBorder +}, localize('editorDropIntoPromptBorder', "Border color of text shown over editors when dragging files. This text informs the user that they can hold shift to drop into the editor.")); + export const SIDE_BY_SIDE_EDITOR_HORIZONTAL_BORDER = registerColor('sideBySideEditor.horizontalBorder', { dark: EDITOR_GROUP_BORDER, light: EDITOR_GROUP_BORDER, diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index aa598f1133d..1db1d451aff 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -22,7 +22,7 @@ export const webviewResourceBaseHost = 'vscode-cdn.net'; export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; -export const webviewGenericCspSource = `https://*.${webviewResourceBaseHost}`; +export const webviewGenericCspSource = `'self' https://*.${webviewResourceBaseHost}`; /** * Construct a uri that can load resources inside a webview diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 1e220e2de14..f49139355d7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -12,13 +12,14 @@ import { Delayer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; -import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon } from 'vs/editor/contrib/find/browser/findWidget'; +import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon, NLS_NO_RESULTS, NLS_MATCHES_LOCATION } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground, errorForeground } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; +import * as strings from 'vs/base/common/strings'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -307,7 +308,12 @@ export abstract class SimpleFindWidget extends Widget { this._matchesCount.className = 'matchesCount'; } this._matchesCount.innerText = ''; - const label = count === undefined || count.resultCount === 0 ? `No Results` : `${count.resultIndex + 1} of ${count.resultCount}`; + let label; + if (count?.resultCount === -1) { + label = ''; + } else { + label = count === undefined || count.resultCount === 0 ? NLS_NO_RESULTS : strings.format(NLS_MATCHES_LOCATION, count.resultIndex + 1, count?.resultCount); + } this._matchesCount.appendChild(document.createTextNode(label)); this._matchesCount.classList.toggle('no-results', !count || count.resultCount === 0); this._findInput?.domNode.insertAdjacentElement('afterend', this._matchesCount); diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 138f276bff7..9cadf75ecce 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -111,7 +111,7 @@ export class SuggestEnabledInput extends Widget implements IThemable { private readonly _onInputDidChange = new Emitter(); readonly onInputDidChange: Event = this._onInputDidChange.event; - protected readonly inputWidget: CodeEditorWidget; + readonly inputWidget: CodeEditorWidget; private readonly inputModel: ITextModel; protected stylingContainer: HTMLDivElement; private placeholderText: HTMLDivElement; diff --git a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts index 306f9b93ce7..353965fd9fa 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/untitledTextEditorHint.ts @@ -19,6 +19,7 @@ import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { EventType as GestureEventType, Gesture } from 'vs/base/browser/touch'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; const $ = dom.$; @@ -32,6 +33,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { constructor( private editor: ICodeEditor, + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -53,7 +55,7 @@ export class UntitledTextEditorHintContribution implements IEditorContribution { const model = this.editor.getModel(); if (model && model.uri.scheme === Schemas.untitled && model.getLanguageId() === PLAINTEXT_LANGUAGE_ID && configValue === 'text') { - this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.commandService, this.configurationService, this.keybindingService); + this.untitledTextHintContentWidget = new UntitledTextEditorHintContentWidget(this.editor, this.editorGroupsService, this.commandService, this.configurationService, this.keybindingService); } } @@ -72,6 +74,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { constructor( private readonly editor: ICodeEditor, + private readonly editorGroupsService: IEditorGroupsService, private readonly commandService: ICommandService, private readonly configurationService: IConfigurationService, private readonly keybindingService: IKeybindingService, @@ -103,6 +106,20 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { if (!this.domNode) { this.domNode = $('.untitled-hint'); this.domNode.style.width = 'max-content'; + + const editorType = $('a.editor-type'); + editorType.style.cursor = 'pointer'; + editorType.innerText = localize('notLookingForTextEditor', "Not looking for a text editor?"); + const selectEditorTypeKeyBinding = this.keybindingService.lookupKeybinding('welcome.showNewFileEntries'); + const selectEditorTypeKeybindingLabel = selectEditorTypeKeyBinding?.getLabel(); + if (selectEditorTypeKeybindingLabel) { + editorType.title = localize('keyboardBindingTooltip', "{0}", selectEditorTypeKeybindingLabel); + } + this.domNode.appendChild(editorType); + + this.domNode.appendChild($('br')); + this.domNode.appendChild($('br')); + const language = $('a.language-mode'); language.style.cursor = 'pointer'; language.innerText = localize('selectAlanguage2', "Select a language"); @@ -112,6 +129,7 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { language.title = localize('keyboardBindingTooltip', "{0}", languageKeybindingLabel); } this.domNode.appendChild(language); + const toGetStarted = $('span'); toGetStarted.innerText = localize('toGetStarted', " to get started. Start typing to dismiss, or ",); this.domNode.appendChild(toGetStarted); @@ -136,6 +154,21 @@ class UntitledTextEditorHintContentWidget implements IContentWidget { this.toDispose.push(dom.addDisposableListener(language, GestureEventType.Tap, languageOnClickOrTap)); this.toDispose.push(Gesture.addTarget(language)); + const chooseEditorOnClickOrTap = async (e: MouseEvent) => { + e.stopPropagation(); + + const activeEditorInput = this.editorGroupsService.activeGroup.activeEditor; + const newEditorSelected = await this.commandService.executeCommand('welcome.showNewFileEntries', { from: 'hint' }); + + // Close the active editor as long as it is untitled (swap the editors out) + if (newEditorSelected && activeEditorInput !== null && activeEditorInput.resource?.scheme === Schemas.untitled) { + this.editorGroupsService.activeGroup.closeEditor(activeEditorInput, { preserveFocus: true }); + } + }; + this.toDispose.push(dom.addDisposableListener(editorType, 'click', chooseEditorOnClickOrTap)); + this.toDispose.push(dom.addDisposableListener(editorType, GestureEventType.Tap, chooseEditorOnClickOrTap)); + this.toDispose.push(Gesture.addTarget(editorType)); + const dontShowOnClickOrTap = () => { this.configurationService.updateValue(untitledTextEditorHintSetting, 'hidden'); this.dispose(); diff --git a/src/vs/workbench/contrib/comments/browser/commentColors.ts b/src/vs/workbench/contrib/comments/browser/commentColors.ts index f906c411aad..78555e0e9e3 100644 --- a/src/vs/workbench/contrib/comments/browser/commentColors.ts +++ b/src/vs/workbench/contrib/comments/browser/commentColors.ts @@ -14,6 +14,8 @@ const resolvedCommentBorder = registerColor('editorCommentsWidget.resolvedBorder const unresolvedCommentBorder = registerColor('editorCommentsWidget.unresolvedBorder', { dark: peekViewBorder, light: peekViewBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('unresolvedCommentBorder', 'Color of borders and arrow for unresolved comments.')); export const commentThreadRangeBackground = registerColor('editorCommentsWidget.rangeBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); export const commentThreadRangeBorder = registerColor('editorCommentsWidget.rangeBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .4) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeActiveBackground = registerColor('editorCommentsWidget.rangeActiveBackground', { dark: transparent(unresolvedCommentBorder, .1), light: transparent(unresolvedCommentBorder, .1), hcDark: transparent(unresolvedCommentBorder, .1), hcLight: transparent(unresolvedCommentBorder, .1) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); +export const commentThreadRangeActiveBorder = registerColor('editorCommentsWidget.rangeActiveBorder', { dark: transparent(unresolvedCommentBorder, .4), light: transparent(unresolvedCommentBorder, .4), hcDark: transparent(unresolvedCommentBorder, .4), hcLight: transparent(unresolvedCommentBorder, .2) }, nls.localize('commentThreadRangeBackground', 'Color of background for comment ranges.')); const commentThreadStateColors = new Map([ [languages.CommentThreadState.Unresolved, unresolvedCommentBorder], @@ -23,8 +25,6 @@ const commentThreadStateColors = new Map([ export const commentThreadStateColorVar = '--comment-thread-state-color'; export const commentViewThreadStateColorVar = '--comment-view-thread-state-color'; export const commentThreadStateBackgroundColorVar = '--comment-thread-state-background-color'; -export const commentThreadRangeBackgroundColorVar = '--vscode-comment-thread-range-background'; -export const commentThreadRangeBorderColorVar = '--vscode-comment-thread-range-border'; export function getCommentThreadStateColor(state: languages.CommentThreadState | undefined, theme: IColorTheme): Color | undefined { const colorId = (state !== undefined) ? commentThreadStateColors.get(state) : undefined; diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index a11e4b8fb15..eed900b49eb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -67,6 +67,7 @@ export interface ICommentService { readonly onDidUpdateCommentThreads: Event; readonly onDidUpdateNotebookCommentThreads: Event; readonly onDidChangeActiveCommentThread: Event; + readonly onDidChangeCurrentCommentThread: Event; readonly onDidUpdateCommentingRanges: Event<{ owner: string }>; readonly onDidChangeActiveCommentingRange: Event<{ range: Range; commentingRangesInfo: CommentingRanges }>; readonly onDidSetDataProvider: Event; @@ -90,6 +91,7 @@ export interface ICommentService { hasReactionHandler(owner: string): boolean; toggleReaction(owner: string, resource: URI, thread: CommentThread, comment: Comment, reaction: CommentReaction): Promise; setActiveCommentThread(commentThread: CommentThread | null): void; + setCurrentCommentThread(commentThread: CommentThread | undefined): void; } export class CommentService extends Disposable implements ICommentService { @@ -119,6 +121,9 @@ export class CommentService extends Disposable implements ICommentService { private readonly _onDidChangeActiveCommentThread = this._register(new Emitter()); readonly onDidChangeActiveCommentThread = this._onDidChangeActiveCommentThread.event; + private readonly _onDidChangeCurrentCommentThread = this._register(new Emitter()); + readonly onDidChangeCurrentCommentThread = this._onDidChangeCurrentCommentThread.event; + private readonly _onDidChangeActiveCommentingRange: Emitter<{ range: Range; commentingRangesInfo: CommentingRanges; @@ -137,6 +142,18 @@ export class CommentService extends Disposable implements ICommentService { super(); } + /** + * The current comment thread is the thread that has focus or is being hovered. + * @param commentThread + */ + setCurrentCommentThread(commentThread: CommentThread | undefined) { + this._onDidChangeCurrentCommentThread.fire(commentThread); + } + + /** + * The active comment thread is the the thread that is currently being edited. + * @param commentThread + */ setActiveCommentThread(commentThread: CommentThread | null) { this._onDidChangeActiveCommentThread.fire(commentThread); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts index fca40378daa..59a1d57b12b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadBody.ts @@ -209,8 +209,14 @@ export class CommentThreadBody extends D } private _updateAriaLabel() { - this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", - this._commentThread.comments?.length, this._commentThread.label); + if (this._commentThread.isDocumentCommentThread()) { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria.withRange', "Comment thread with {0} comments on lines {1} through {2}. {3}.", + this._commentThread.comments?.length, this._commentThread.range.startLineNumber, this._commentThread.range.endLineNumber, + this._commentThread.label); + } else { + this._commentsElement.ariaLabel = nls.localize('commentThreadAria', "Comment thread with {0} comments. {1}.", + this._commentThread.comments?.length, this._commentThread.label); + } } private _setFocusedComment(value: number | undefined) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts index adadb4a6617..5af04a9fe0c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadRangeDecorator.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IRange } from 'vs/editor/common/core/range'; import { IModelDecorationOptions, IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICommentInfo } from 'vs/workbench/contrib/comments/browser/commentService'; +import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; class CommentThreadRangeDecoration implements IModelDeltaDecoration { private _decorationId: string | undefined; @@ -26,12 +27,16 @@ class CommentThreadRangeDecoration implements IModelDeltaDecoration { } } -export class CommentThreadRangeDecorator { - public static description = 'comment-thread-range-decorator'; +export class CommentThreadRangeDecorator extends Disposable { + private static description = 'comment-thread-range-decorator'; private decorationOptions: ModelDecorationOptions; + private activeDecorationOptions: ModelDecorationOptions; private decorationIds: string[] = []; + private activeDecorationIds: string[] = []; + private editor: ICodeEditor | undefined; - constructor() { + constructor(commentService: ICommentService) { + super(); const decorationOptions: IModelDecorationOptions = { description: CommentThreadRangeDecorator.description, isWholeLine: false, @@ -40,6 +45,29 @@ export class CommentThreadRangeDecorator { }; this.decorationOptions = ModelDecorationOptions.createDynamic(decorationOptions); + + const activeDecorationOptions: IModelDecorationOptions = { + description: CommentThreadRangeDecorator.description, + isWholeLine: false, + zIndex: 20, + className: 'comment-thread-range-current' + }; + + this.activeDecorationOptions = ModelDecorationOptions.createDynamic(activeDecorationOptions); + this._register(commentService.onDidChangeCurrentCommentThread(thread => { + if (!this.editor) { + return; + } + let newDecoration: CommentThreadRangeDecoration[] = []; + if (thread) { + const range = thread.range; + if (!((range.startLineNumber === range.endLineNumber) && (range.startColumn === range.endColumn))) { + newDecoration.push(new CommentThreadRangeDecoration(range, this.activeDecorationOptions)); + } + } + this.activeDecorationIds = this.editor.deltaDecorations(this.activeDecorationIds, newDecoration); + newDecoration.forEach((decoration, index) => decoration.id = this.decorationIds[index]); + })); } public update(editor: ICodeEditor, commentInfos: ICommentInfo[]) { @@ -47,6 +75,7 @@ export class CommentThreadRangeDecorator { if (!model) { return; } + this.editor = editor; const commentThreadRangeDecorations: CommentThreadRangeDecoration[] = []; for (const info of commentInfos) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 0f9f5fd1500..92b06773800 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -109,6 +109,41 @@ export class CommentThreadWidget extends if (controller) { commentControllerKey.set(controller.contextValue); } + + this.currentThreadListeners(); + } + + private updateCurrentThread(hasMouse: boolean, hasFocus: boolean) { + if (hasMouse || hasFocus) { + this.commentService.setCurrentCommentThread(this.commentThread); + } else { + this.commentService.setCurrentCommentThread(undefined); + } + } + + private currentThreadListeners() { + let hasMouse = false; + let hasFocus = false; + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_ENTER, (e) => { + if ((e).toElement === this.container) { + hasMouse = true; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_LEAVE, (e) => { + if ((e).fromElement === this.container) { + hasMouse = false; + this.updateCurrentThread(hasMouse, hasFocus); + } + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_IN, () => { + hasFocus = true; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); + this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_OUT, () => { + hasFocus = false; + this.updateCurrentThread(hasMouse, hasFocus); + }, true)); } updateCommentThread(commentThread: languages.CommentThread) { diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 0e41198ccb2..c6a047179b0 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -193,7 +193,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._scopedInstantiationService, this._commentThread as unknown as languages.CommentThread, this._pendingComment, - { editor: this.editor }, + { editor: this.editor, codeBlockFontSize: '' }, this._commentOptions, { actionRunner: () => { diff --git a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts index 0d515f2ae79..2ba619a7c0c 100644 --- a/src/vs/workbench/contrib/comments/browser/comments.contribution.ts +++ b/src/vs/workbench/contrib/comments/browser/comments.contribution.ts @@ -24,9 +24,9 @@ Registry.as(ConfigurationExtensions.Configuration).regis markdownDeprecationMessage: nls.localize('comments.openPanel.deprecated', "This setting is deprecated in favor of `comments.openView`.") }, 'comments.openView': { - enum: ['never', 'file'], - enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active.")], - default: 'file', + enum: ['never', 'file', 'firstFile'], + enumDescriptions: [nls.localize('comments.openView.never', "The comments view will never be opened."), nls.localize('comments.openView.file', "The comments view will open when a file with comments is active."), nls.localize('comments.openView.firstFile', "If the comments view has not been opened yet during this session it will open the first time during a session that a file with comments is active.")], + default: 'firstFile', description: nls.localize('comments.openView', "Controls when the comments view should open."), restricted: false }, diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 0fa7eaf9ded..5d80b0e5dfd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -47,8 +47,9 @@ import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/cont import { Position } from 'vs/editor/common/core/position'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CommentThreadRangeDecorator } from 'vs/workbench/contrib/comments/browser/commentThreadRangeDecorator'; -import { commentThreadRangeBackground, commentThreadRangeBorder } from 'vs/workbench/contrib/comments/browser/commentColors'; +import { commentThreadRangeActiveBackground, commentThreadRangeActiveBorder, commentThreadRangeBackground, commentThreadRangeBorder } from 'vs/workbench/contrib/comments/browser/commentColors'; import { ICursorSelectionChangedEvent } from 'vs/editor/common/cursorEvents'; +import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView'; export const ID = 'editor.contrib.review'; @@ -342,7 +343,7 @@ export class CommentController implements IEditorContribution { } })); - this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(); + this.globalToDispose.add(this._commentThreadRangeDecorator = new CommentThreadRangeDecorator(this.commentService)); this.globalToDispose.add(this.commentService.onDidDeleteDataProvider(ownerId => { delete this._pendingCommentCache[ownerId]; @@ -633,13 +634,24 @@ export class CommentController implements IEditorContribution { })); this.beginCompute().then(() => { - if (this._commentWidgets.length - && (this.configurationService.getValue(COMMENTS_SECTION).openView === 'file')) { - this.viewsService.openView(COMMENTS_VIEW_ID); - } + return this.openCommentsView(); }); } + private async openCommentsView() { + if (this._commentWidgets.length) { + if (this.configurationService.getValue(COMMENTS_SECTION).openView === 'file') { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } else if (this.configurationService.getValue(COMMENTS_SECTION).openView === 'firstFile') { + const hasShownView = this.viewsService.getViewWithId(COMMENTS_VIEW_ID)?.hasRendered; + if (!hasShownView) { + return this.viewsService.openView(COMMENTS_VIEW_ID); + } + } + } + return undefined; + } + private displayCommentThread(owner: string, thread: languages.CommentThread, pendingComment: string | null): void { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, owner, thread, pendingComment); zoneWidget.display(thread.range.endLineNumber); @@ -1092,4 +1104,18 @@ registerThemingParticipant((theme, collector) => { border-style: solid; box-sizing: border-box; }`); } + + const commentThreadRangeActiveBackgroundColor = theme.getColor(commentThreadRangeActiveBackground); + if (commentThreadRangeActiveBackgroundColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { background-color: ${commentThreadRangeActiveBackgroundColor};}`); + } + + const commentThreadRangeActiveBorderColor = theme.getColor(commentThreadRangeActiveBorder); + if (commentThreadRangeActiveBorderColor) { + collector.addRule(`.monaco-editor .comment-thread-range-current { + border-color: ${commentThreadRangeActiveBorderColor}; + border-width: 1px; + border-style: solid; + box-sizing: border-box; }`); + } }); diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index ebe8b7b9de2..940de2d18d7 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -58,6 +58,7 @@ interface ICommentThreadTemplateData { timestamp: TimestampWidget; separator: HTMLElement; commentPreview: HTMLSpanElement; + range: HTMLSpanElement; }; repliesMetadata: { container: HTMLElement; @@ -138,7 +139,8 @@ export class CommentNodeRenderer implements IListRenderer userNames: dom.append(metadataContainer, dom.$('.user')), timestamp: new TimestampWidget(this.configurationService, dom.append(metadataContainer, dom.$('.timestamp-container'))), separator: dom.append(metadataContainer, dom.$('.separator')), - commentPreview: dom.append(metadataContainer, dom.$('.text')) + commentPreview: dom.append(metadataContainer, dom.$('.text')), + range: dom.append(metadataContainer, dom.$('.range')) }; data.threadMetadata.separator.innerText = '\u00b7'; @@ -210,6 +212,12 @@ export class CommentNodeRenderer implements IListRenderer templateData.threadMetadata.commentPreview.title = renderedComment.element.textContent ?? ''; } + if (node.element.range.startLineNumber === node.element.range.endLineNumber) { + templateData.threadMetadata.range.textContent = nls.localize('commentLine', "[Ln {0}]", node.element.range.startLineNumber); + } else { + templateData.threadMetadata.range.textContent = nls.localize('commentRange', "[Ln {0}-{1}]", node.element.range.startLineNumber, node.element.range.endLineNumber); + } + if (!node.element.hasReply()) { templateData.repliesMetadata.container.style.display = 'none'; return; @@ -217,9 +225,9 @@ export class CommentNodeRenderer implements IListRenderer templateData.repliesMetadata.container.style.display = ''; templateData.repliesMetadata.count.textContent = this.getCountString(commentCount); - templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", node.element.replies[node.element.replies.length - 1].comment.userName); - templateData.repliesMetadata.timestamp.setTimestamp(originalComment.comment.timestamp ? new Date(originalComment.comment.timestamp) : undefined); - + const lastComment = node.element.replies[node.element.replies.length - 1].comment; + templateData.repliesMetadata.lastReplyDetail.textContent = nls.localize('lastReplyFrom', "Last reply from {0}", lastComment.userName); + templateData.repliesMetadata.timestamp.setTimestamp(lastComment.timestamp ? new Date(lastComment.timestamp) : undefined); } private getCommentThreadWidgetStateColor(state: CommentThreadState | undefined, theme: IColorTheme): Color | undefined { diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index ad852ea3b7e..7077d35338b 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -120,7 +120,7 @@ export class CommentsPanel extends ViewPane { const focusColor = theme.getColor(focusBorder); if (focusColor) { - content.push(`.comments-panel .commenst-panel-container a:focus { outline-color: ${focusColor}; }`); + content.push(`.comments-panel .comments-panel-container a:focus { outline-color: ${focusColor}; }`); } const codeTextForegroundColor = theme.getColor(textPreformatForeground); @@ -147,6 +147,10 @@ export class CommentsPanel extends ViewPane { } } + public get hasRendered(): boolean { + return !!this.tree; + } + public override layoutBody(height: number, width: number): void { super.layoutBody(height, width); this.tree.layout(height, width); diff --git a/src/vs/workbench/contrib/comments/browser/media/panel.css b/src/vs/workbench/contrib/comments/browser/media/panel.css index 8b2e685cb8b..37bc9a17a45 100644 --- a/src/vs/workbench/contrib/comments/browser/media/panel.css +++ b/src/vs/workbench/contrib/comments/browser/media/panel.css @@ -49,8 +49,7 @@ .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .count, .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-metadata-container .user { - overflow: hidden; - text-overflow: ellipsis; + min-width: fit-content; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text { @@ -74,6 +73,11 @@ text-overflow: ellipsis; max-width: 500px; overflow: hidden; + padding-right: 5px; +} + +.comments-panel .comments-panel-container .tree-container .comment-thread-container .range { + opacity: 0.8; } .comments-panel .comments-panel-container .tree-container .comment-thread-container .comment-snippet-container .text code { diff --git a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts index 1ef2ba67cb7..d3c180e69a4 100644 --- a/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts +++ b/src/vs/workbench/contrib/comments/common/commentsConfiguration.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ export interface ICommentsConfiguration { - openView: 'never' | 'file'; + openView: 'never' | 'file' | 'firstFile'; useRelativeTime: boolean; } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 119a41914f1..81dfae861af 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -124,6 +124,8 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { public override get capabilities(): EditorInputCapabilities { let capabilities = EditorInputCapabilities.None; + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + if (!this.customEditorService.getCustomEditorCapabilities(this.viewType)?.supportsMultipleEditorsPerDocument) { capabilities |= EditorInputCapabilities.Singleton; } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index acdf0b9005d..e97efddd569 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -9,6 +9,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; +import { Codicon } from 'vs/base/common/codicons'; import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import { once } from 'vs/base/common/functional'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -156,9 +157,9 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + const isShiftPressed = e.event.shiftKey; + const enabled = breakpoints.some(bp => bp.enabled); + + if (isShiftPressed) { + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); + } else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + // Show the dialog if there is a potential condition to be accidently lost. + // Do not show dialog on linux due to electron issue freezing the mouse #50026 const logPoint = breakpoints.every(bp => !!bp.logMessage); const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint"); - const disable = breakpoints.some(bp => bp.enabled); - const enabling = nls.localize('breakpointHasConditionDisabled', + const disabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionDisabled', "This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const disabling = nls.localize('breakpointHasConditionEnabled', + const enabledBreakpointDialogMessage = nls.localize( + 'breakpointHasConditionEnabled', "This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.", breakpointType.toLowerCase(), logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition") ); - const { choice } = await this.dialogService.show(severity.Info, disable ? disabling : enabling, [ - nls.localize('removeLogPoint', "Remove {0}", breakpointType), - nls.localize('disableLogPoint', "{0} {1}", disable ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), - nls.localize('cancel', "Cancel") - ], { cancelId: 2 }); + const { choice } = await this.dialogService.show( + severity.Info, + enabled ? enabledBreakpointDialogMessage : disabledBreakpointDialogMessage, + [ + nls.localize('removeLogPoint', "Remove {0}", breakpointType), + nls.localize('disableLogPoint', "{0} {1}", enabled ? nls.localize('disable', "Disable") : nls.localize('enable', "Enable"), breakpointType), + nls.localize('cancel', "Cancel") + ], + { cancelId: 2 }, + ); if (choice === 0) { breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId())); } if (choice === 1) { - breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!disable, bp)); + breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } } else { - const enabled = breakpoints.some(bp => bp.enabled); if (!enabled) { breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); } else { @@ -485,7 +495,6 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); - } finally { this.ignoreDecorationsChangedEvent = false; } @@ -511,6 +520,12 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi inlineWidget }; }); + + for (const d of this.breakpointDecorations) { + if (d.inlineWidget) { + this.editor.layoutContentWidget(d.inlineWidget); + } + } } private async onModelDecorationsChanged(): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index b23aa7e10af..6a045d3ce23 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -47,7 +47,6 @@ import { createDisconnectMenuItemAction } from 'vs/workbench/contrib/debug/brows import { CALLSTACK_VIEW_ID, CONTEXT_CALLSTACK_ITEM_STOPPED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_CALLSTACK_SESSION_HAS_ONE_THREAD, CONTEXT_CALLSTACK_SESSION_IS_ATTACH, CONTEXT_DEBUG_STATE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, getStateLabel, IDebugModel, IDebugService, IDebugSession, IRawStoppedDetails, IStackFrame, IThread, State } from 'vs/workbench/contrib/debug/common/debug'; import { StackFrame, Thread, ThreadAndSessionIds } from 'vs/workbench/contrib/debug/common/debugModel'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; const $ = dom.$; @@ -150,7 +149,6 @@ export class CallStackView extends ViewPane { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IEditorService private readonly editorService: IEditorService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @@ -287,10 +285,10 @@ export class CallStackView extends ViewPane { return; } - const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession) => { + const focusStackFrame = (stackFrame: IStackFrame | undefined, thread: IThread | undefined, session: IDebugSession, options: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean } = {}) => { this.ignoreFocusStackFrameEvent = true; try { - this.debugService.focusStackFrame(stackFrame, thread, session, true); + this.debugService.focusStackFrame(stackFrame, thread, session, { ...options, ...{ explicit: true } }); } finally { this.ignoreFocusStackFrameEvent = false; } @@ -298,8 +296,12 @@ export class CallStackView extends ViewPane { const element = e.element; if (element instanceof StackFrame) { - focusStackFrame(element, element.thread, element.thread.session); - element.openInEditor(this.editorService, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned); + const opts = { + preserveFocus: e.editorOptions.preserveFocus, + sideBySide: e.sideBySide, + pinned: e.editorOptions.pinned + }; + focusStackFrame(element, element.thread, element.thread.session, opts); } if (element instanceof Thread) { focusStackFrame(undefined, element, element.session); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 03a03efeb2a..d595b7768d1 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -511,6 +511,11 @@ configurationRegistry.registerConfiguration({ description: nls.localize('debug.focusWindowOnBreak', "Controls whether the workbench window should be focused when the debugger breaks."), default: true }, + 'debug.focusEditorOnBreak': { + type: 'boolean', + description: nls.localize('debug.focusEditorOnBreak', "Controls whether the editor should be focused when the debugger breaks."), + default: true + }, 'debug.onTaskErrors': { enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], @@ -553,5 +558,10 @@ configurationRegistry.registerConfiguration({ default: true, description: nls.localize('debug.disassemblyView.showSourceCode', "Show Source Code in Disassembly View.") }, + 'debug.autoExpandLazyVariables': { + type: 'boolean', + default: false, + description: nls.localize('debug.autoExpandLazyVariables', "Automatically show values for variables that are lazily resolved by the debugger, such as getters.") + } } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 2a01435a6f5..24c64bc9f81 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -10,8 +10,8 @@ import Severity from 'vs/base/common/severity'; import * as strings from 'vs/base/common/strings'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorModel } from 'vs/editor/common/editorCommon'; -import { ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel } from 'vs/editor/common/model'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -22,6 +22,7 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { Breakpoints } from 'vs/workbench/contrib/debug/common/breakpoints'; import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; @@ -42,7 +43,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { private debugExtensionsAvailable: IContextKey; private readonly _onDidRegisterDebugger = new Emitter(); private readonly _onDidDebuggersExtPointRead = new Emitter(); - private breakpointLanguageIdsSet = new Set(); + private breakpointContributions: Breakpoints[] = []; private debuggerWhenKeys = new Set(); constructor( @@ -116,13 +117,8 @@ export class AdapterManager extends Disposable implements IAdapterManager { this._onDidDebuggersExtPointRead.fire(); }); - breakpointsExtPoint.setHandler((extensions, delta) => { - delta.removed.forEach(removed => { - removed.value.forEach(breakpoints => this.breakpointLanguageIdsSet.delete(breakpoints.language)); - }); - delta.added.forEach(added => { - added.value.forEach(breakpoints => this.breakpointLanguageIdsSet.add(breakpoints.language)); - }); + breakpointsExtPoint.setHandler(extensions => { + this.breakpointContributions = extensions.flatMap(ext => ext.value.map(breakpoint => this.instantiationService.createInstance(Breakpoints, breakpoint))); }); } @@ -281,7 +277,7 @@ export class AdapterManager extends Disposable implements IAdapterManager { return true; } - return this.breakpointLanguageIdsSet.has(languageId); + return this.breakpointContributions.some(breakpoints => breakpoints.language === languageId && breakpoints.enabled); } getDebugger(type: string): Debugger | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/debugColors.ts b/src/vs/workbench/contrib/debug/browser/debugColors.ts index affb86d4e98..3ebe8126973 100644 --- a/src/vs/workbench/contrib/debug/browser/debugColors.ts +++ b/src/vs/workbench/contrib/debug/browser/debugColors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, foreground, editorInfoForeground, editorWarningForeground, errorForeground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; import { localize } from 'vs/nls'; @@ -125,6 +125,7 @@ export function registerColors() { const debugViewStateLabelForegroundColor = theme.getColor(debugViewStateLabelForeground)!; const debugViewStateLabelBackgroundColor = theme.getColor(debugViewStateLabelBackground)!; const debugViewValueChangedHighlightColor = theme.getColor(debugViewValueChangedHighlight)!; + const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); collector.addRule(` /* Text colour of the call stack row's filename */ @@ -182,6 +183,10 @@ export function registerColors() { animation-duration: 1s; animation-fill-mode: forwards; } + + .monaco-list-row .expression .lazy-button:hover { + background-color: ${toolbarHoverBackgroundColor} + } `); const contrastBorderColor = theme.getColor(contrastBorder); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 5f6e41bf0c6..3042792f8e8 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -406,7 +406,7 @@ CommandsRegistry.registerCommand({ if (stoppedChildSession && session.state !== State.Stopped) { session = stoppedChildSession; } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); const stackFrame = debugService.getViewModel().focusedStackFrame; if (stackFrame) { await stackFrame.openInEditor(editorService, true); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index d2665b688bd..224b72fe4fa 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -852,11 +852,11 @@ export class DebugService implements IDebugService { //---- focus management - async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, explicit?: boolean): Promise { + async focusStackFrame(_stackFrame: IStackFrame | undefined, _thread?: IThread, _session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise { const { stackFrame, thread, session } = getStackFrameThreadAndSessionToFocus(this.model, _stackFrame, _thread, _session); if (stackFrame) { - const editor = await stackFrame.openInEditor(this.editorService, true); + const editor = await stackFrame.openInEditor(this.editorService, options?.preserveFocus ?? true, options?.sideBySide, options?.pinned); if (editor) { if (editor.input === DisassemblyViewInput.instance) { // Go to address is invoked via setFocus @@ -880,7 +880,7 @@ export class DebugService implements IDebugService { this.debugType.reset(); } - this.viewModel.setFocus(stackFrame, thread, session, !!explicit); + this.viewModel.setFocus(stackFrame, thread, session, !!options?.explicit); } //---- watches diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 9109c4f0145..3ff60d94d24 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -172,6 +172,11 @@ export class DebugSession implements IDebugSession { return this._options.debugUI?.simple ?? false; } + get autoExpandLazyVariables(): boolean { + // This tiny helper avoids converting the entire debug model to use service injection + return this.configurationService.getValue('debug').autoExpandLazyVariables; + } + setConfiguration(configuration: { resolved: IConfig; unresolved: IConfig | undefined }) { this._configuration = configuration; } @@ -958,7 +963,8 @@ export class DebugSession implements IDebugSession { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; if (!focusedStackFrame || focusedStackFrame.thread.session === this) { // Only take focus if nothing is focused, or if the focus is already on the current session - await this.debugService.focusStackFrame(undefined, thread); + const preserveFocus = !this.configurationService.getValue('debug').focusEditorOnBreak; + await this.debugService.focusStackFrame(undefined, thread, undefined, { preserveFocus }); } if (thread.stoppedDetails) { @@ -1004,7 +1010,7 @@ export class DebugSession implements IDebugSession { this.passFocusScheduler.cancel(); if (focusedThread && event.body.threadId === focusedThread.threadId) { // De-focus the thread in case it was focused - this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, false); + this.debugService.focusStackFrame(undefined, undefined, viewModel.focusedSession, { explicit: false }); } } })); @@ -1071,7 +1077,7 @@ export class DebugSession implements IDebugSession { // only log telemetry events from debug adapter if the debug extension provided the telemetry key // and the user opted in telemetry const telemetryEndpoint = this.raw.dbgr.getCustomTelemetryEndpoint(); - if (telemetryEndpoint && this.telemetryService.telemetryLevel !== TelemetryLevel.NONE) { + if (telemetryEndpoint && this.telemetryService.telemetryLevel.value !== TelemetryLevel.NONE) { // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. let data = event.body.data; if (!telemetryEndpoint.sendErrorTelemetry && event.body.data) { diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 43865553172..577a80c532e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -84,11 +84,14 @@ font-size: 11px; } -.monaco-workbench .monaco-list-row .expression .value, -.monaco-workbench .monaco-list-row .expression .lazy-button { +.monaco-workbench .monaco-list-row .expression .value { margin-left: 6px; } +.monaco-workbench .monaco-list-row .expression .lazy-button { + margin-left: 3px; +} + /* Links */ .monaco-workbench .monaco-list-row .expression .value a.link:hover { @@ -119,20 +122,14 @@ .monaco-workbench .monaco-list-row .expression .lazy-button { display: none; -} - -.monaco-workbench .monaco-list-row .expression .lazy-button:hover { - text-decoration: underline; + border-radius: 5px; + padding: 3px; } .monaco-workbench .monaco-list-row .expression.lazy .lazy-button { display: inline; } -.monaco-workbench .monaco-list-row .expression.lazy .value { - display: none; -} - .monaco-workbench .debug-inline-value { background-color: var(--vscode-editor-inlineValuesBackground); color: var(--vscode-editor-inlineValuesForeground); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index cc2a06f698e..d171c967d4f 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -895,7 +895,7 @@ registerAction2(class extends ViewAction { session = stopppedChildSession; } } - await debugService.focusStackFrame(undefined, undefined, session, true); + await debugService.focusStackFrame(undefined, undefined, session, { explicit: true }); } // Need to select the session in the view since the focussed session might not have changed await view.selectSession(session); diff --git a/src/vs/workbench/contrib/debug/common/breakpoints.ts b/src/vs/workbench/contrib/debug/common/breakpoints.ts new file mode 100644 index 00000000000..38bc0d5114d --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/breakpoints.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; + +export class Breakpoints { + + private breakpointsWhen: ContextKeyExpression | undefined; + + constructor( + private readonly breakpointContribution: IBreakpointContribution, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + this.breakpointsWhen = typeof breakpointContribution.when === 'string' ? ContextKeyExpr.deserialize(breakpointContribution.when) : undefined; + } + + get language(): string { + return this.breakpointContribution.language; + } + + get enabled(): boolean { + return !this.breakpointsWhen || this.contextKeyService.contextMatchesRules(this.breakpointsWhen); + } +} diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index b8e653916c5..9457ae8c1b4 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -291,6 +291,7 @@ export interface IDebugSession extends ITreeElement { readonly compoundRoot: DebugCompoundRoot | undefined; readonly name: string; readonly isSimpleUI: boolean; + readonly autoExpandLazyVariables: boolean; setSubId(subId: string | undefined): void; @@ -630,6 +631,7 @@ export interface IDebugConfiguration { acceptSuggestionOnEnter: 'off' | 'on'; }; focusWindowOnBreak: boolean; + focusEditorOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt' | 'abort'; showBreakpointsInOverviewRuler: boolean; showInlineBreakpointCandidates: boolean; @@ -637,6 +639,7 @@ export interface IDebugConfiguration { disassemblyView: { showSourceCode: boolean; }; + autoExpandLazyVariables: boolean; } export interface IGlobalConfig { @@ -771,6 +774,11 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut when?: string; } +export interface IBreakpointContribution { + language: string; + when?: string; +} + export enum DebugConfigurationProviderTriggerKind { /** * `DebugConfigurationProvider.provideDebugConfigurations` is called to provide the initial debug configurations for a newly created launch.json. @@ -951,7 +959,7 @@ export interface IDebugService { /** * Sets the focused stack frame and evaluates all expressions against the newly focused stack frame, */ - focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, explicit?: boolean): Promise; + focusStackFrame(focusedStackFrame: IStackFrame | undefined, thread?: IThread, session?: IDebugSession, options?: { explicit?: boolean; preserveFocus?: boolean; sideBySide?: boolean; pinned?: boolean }): Promise; /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 3e2cb2e1a16..c05fc04c956 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -152,7 +152,7 @@ export class ExpressionContainer implements IExpressionContainer { } const nameCount = new Map(); - return response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { + const vars = response.body.variables.filter(v => !!v).map((v: IDebugProtocolVariableWithContext) => { if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { const count = nameCount.get(v.name) || 0; const idDuplicationIndex = count > 0 ? count.toString() : ''; @@ -161,6 +161,12 @@ export class ExpressionContainer implements IExpressionContainer { } return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false); }); + + if (this.session!.autoExpandLazyVariables) { + await Promise.all(vars.map(v => v.presentationHint?.lazy && v.evaluateLazy())); + } + + return vars; } catch (e) { return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, undefined, { kind: 'virtual' }, undefined, undefined, false)]; } diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 3613a1b44a4..a2a9c7f5480 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -5,7 +5,7 @@ import * as extensionsRegistry from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as nls from 'vs/nls'; -import { IDebuggerContribution, ICompound } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, ICompound, IBreakpointContribution } from 'vs/workbench/contrib/debug/common/debug'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { inputsSchema } from 'vs/workbench/services/configurationResolver/common/configurationResolverSchema'; @@ -68,7 +68,7 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE type: 'object' }, when: { - description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension defined context key as appropriate for this."), + description: nls.localize('vscode.extension.contributes.debuggers.when', "Condition which must be true to enable this type of debugger. Consider using 'shellExecutionSupported', 'virtualWorkspace', 'resourceScheme' or an extension-defined context key as appropriate for this."), type: 'string', default: '' }, @@ -107,12 +107,8 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE } }); -export interface IRawBreakpointContribution { - language: string; -} - // breakpoints extension point #9037 -export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ +export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'breakpoints', jsonSchema: { description: nls.localize('vscode.extension.contributes.breakpoints', 'Contributes breakpoints.'), @@ -127,6 +123,11 @@ export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registe description: nls.localize('vscode.extension.contributes.breakpoints.language', "Allow breakpoints for this language."), type: 'string' }, + when: { + description: nls.localize('vscode.extension.contributes.breakpoints.when', "Condition which must be true to enable breakpoints in this language. Consider matching this to the debugger when clause as appropriate."), + type: 'string', + default: '' + } } } } diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 6347d7ba2a5..4f0bf897787 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -13,7 +13,7 @@ import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 5f89e708bc7..5418d9e8ffc 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -121,7 +121,8 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? quote = (s: string) => { s = s.replace(/\"/g, '""'); - return (' "> s.includes(char)) || s.length === 0) ? `"${s}"` : s; + s = s.replace(/([> s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; if (cwd) { @@ -138,7 +139,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? if (value === null) { command += `set "${key}=" && `; } else { - value = value.replace(/[\^\&\|\<\>]/g, s => `^${s}`); + value = value.replace(/[&^|<>]/g, s => `^${s}`); command += `set "${key}=${value}" && `; } } @@ -154,7 +155,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? case ShellType.bash: { quote = (s: string) => { - s = s.replace(/(["'\\\$!><#()\[\]*&^])/g, '\\$1'); + s = s.replace(/(["'\\\$!><#()\[\]*&^|])/g, '\\$1'); return (' ;'.split('').some(char => s.includes(char)) || s.length === 0) ? `"${s}"` : s; }; diff --git a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts index 81c394466ec..1bebaa31adb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/browser/mockDebug.ts @@ -168,6 +168,8 @@ export class MockDebugService implements IDebugService { } export class MockSession implements IDebugSession { + readonly autoExpandLazyVariables = false; + getMemory(memoryReference: string): IMemoryRegion { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 41515d1c2e2..ebbd930216f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -200,9 +200,12 @@ Registry.as(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': false - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': false + } + }] }, 'extensions.experimental.affinity': { type: 'object', @@ -214,9 +217,12 @@ Registry.as(ConfigurationExtensions.Configuration) } }, additionalProperties: false, - default: { - 'pub.name': 1 - } + default: {}, + defaultSnippets: [{ + 'body': { + 'pub.name': 1 + } + }] }, [WORKSPACE_TRUST_EXTENSION_SUPPORT]: { type: 'object', diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 50152dc1f51..701473fe624 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -206,6 +206,10 @@ export class Extension implements IExtension { if (!this.gallery || !this.local) { return false; } + // Do not allow updating system extensions in stable + if (this.type === ExtensionType.System && this.productService.quality === 'stable') { + return false; + } if (!this.local.preRelease && this.gallery.properties.isPreReleaseVersion) { return false; } @@ -1057,8 +1061,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension // Skip if check updates only for builtin extensions and current extension is not builtin. continue; } - if (installed.isBuiltin && !installed.local?.identifier.uuid) { - // Skip if the builtin extension does not have Marketplace id + if (installed.isBuiltin && (!installed.local?.identifier.uuid || this.productService.quality !== 'stable')) { + // Skip if the builtin extension does not have Marketplace identifier or the current quality is not stable. continue; } infos.push({ ...installed.identifier, preRelease: !!installed.local?.preRelease }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts index 335679af2a9..f07341fe991 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extension.test.ts @@ -12,6 +12,7 @@ import { URI } from 'vs/base/common/uri'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { generateUuid } from 'vs/base/common/uuid'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { IProductService } from 'vs/platform/product/common/productService'; suite('Extension Test', () => { @@ -19,6 +20,7 @@ suite('Extension Test', () => { setup(() => { instantiationService = new TestInstantiationService(); + instantiationService.stub(IProductService, >{ quality: 'insiders' }); }); test('extension is not outdated when there is no local and gallery', () => { @@ -51,6 +53,12 @@ suite('Extension Test', () => { assert.strictEqual(extension.outdated, true); }); + test('extension is not outdated when local is built in and older than gallery but product quality is stable', () => { + instantiationService.stub(IProductService, >{ quality: 'stable' }); + const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', { version: '1.0.0' }, { type: ExtensionType.System }), aGalleryExtension('somext', { version: '1.0.1' })); + assert.strictEqual(extension.outdated, false); + }); + test('extension is outdated when local and gallery are on same version but on different target platforms', () => { const extension = instantiationService.createInstance(Extension, () => ExtensionState.Installed, undefined, aLocalExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_IA32 }), aGalleryExtension('somext', {}, { targetPlatform: TargetPlatform.WIN32_X64 })); assert.strictEqual(extension.outdated, true); diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 630cd3a523a..b6317eb2dd7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -60,6 +60,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index d88bd1c2d2b..71422933855 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -576,7 +576,7 @@ MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { }); // Empty Editor Group Context Menu -MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New File") }, group: '1_file', order: 10 }); +MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: NEW_UNTITLED_FILE_COMMAND_ID, title: nls.localize('newFile', "New Text File") }, group: '1_file', order: 10 }); MenuRegistry.appendMenuItem(MenuId.EmptyEditorGroupContext, { command: { id: 'workbench.action.quickOpen', title: nls.localize('openFile', "Open File...") }, group: '1_file', order: 20 }); // File menu @@ -585,7 +585,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { group: '1_new', command: { id: NEW_UNTITLED_FILE_COMMAND_ID, - title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New File") + title: nls.localize({ key: 'miNewFile', comment: ['&& denotes a mnemonic'] }, "&&New Text File") }, order: 1 }); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 1a6f2c8f672..c113c495f27 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -337,10 +337,16 @@ configurationRegistry.registerConfiguration({ 'properties': { 'explorer.openEditors.visible': { 'type': 'number', - 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "Number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), + 'description': nls.localize({ key: 'openEditorsVisible', comment: ['Open is an adjective'] }, "The maximum number of editors shown in the Open Editors pane. Setting this to 0 hides the Open Editors pane."), 'default': 9, 'minimum': 0 }, + 'explorer.openEditors.minVisible': { + 'type': 'number', + 'description': nls.localize({ key: 'openEditorsVisibleMin', comment: ['Open is an adjective'] }, "The minimum number of editor slots shown in the Open Editors pane. If set to 0 the Open Editors pane will dynamically resize based on the number of editors."), + 'default': 0, + 'minimum': 0 + }, 'explorer.openEditors.sortOrder': { 'type': 'string', 'enum': ['editorOrder', 'alphabetical', 'fullPath'], diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index b7bc23b3257..06ba5acdbde 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -59,6 +59,7 @@ const $ = dom.$; export class OpenEditorsView extends ViewPane { private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9; + private static readonly DEFAULT_MIN_VISIBLE_OPEN_EDITORS = 0; static readonly ID = 'workbench.explorer.openEditorsView'; static readonly NAME = nls.localize({ key: 'openEditors', comment: ['Open is an adjective'] }, "Open Editors"); @@ -466,12 +467,17 @@ export class OpenEditorsView extends ViewPane { } private getMaxExpandedBodySize(): number { + let minVisibleOpenEditors = this.configurationService.getValue('explorer.openEditors.minVisible'); + // If it's not a number setting it to 0 will result in dynamic resizing. + if (typeof minVisibleOpenEditors !== 'number') { + minVisibleOpenEditors = OpenEditorsView.DEFAULT_MIN_VISIBLE_OPEN_EDITORS; + } const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; if (containerModel.visibleViewDescriptors.length <= 1) { return Number.POSITIVE_INFINITY; } - return this.elementCount * OpenEditorsDelegate.ITEM_HEIGHT; + return (Math.max(this.elementCount, minVisibleOpenEditors)) * OpenEditorsDelegate.ITEM_HEIGHT; } private getMinExpandedBodySize(): number { diff --git a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts index 6fc249b03be..5f6cdfb3298 100644 --- a/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts +++ b/src/vs/workbench/contrib/keybindings/browser/keybindings.contribution.ts @@ -9,7 +9,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; class ToggleKeybindingsLogAction extends Action2 { diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts new file mode 100644 index 00000000000..3df19dec7a5 --- /dev/null +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; +import { ThrottledDelayer } from 'vs/base/common/async'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { NOTEBOOK_EDITOR_EDITABLE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { Schemas } from 'vs/base/common/network'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + +const detectLanguageCommandId = 'editor.detectLanguage'; + +class LanguageDetectionStatusContribution implements IWorkbenchContribution { + + private static readonly _id = 'status.languageDetectionStatus'; + + private readonly _disposables = new DisposableStore(); + private _combinedEntry?: IStatusbarEntryAccessor; + private _delayer = new ThrottledDelayer(1000); + private _renderDisposables = new DisposableStore(); + + constructor( + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IStatusbarService private readonly _statusBarService: IStatusbarService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService private readonly _editorService: IEditorService, + @ILanguageService private readonly _languageService: ILanguageService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + _editorService.onDidActiveEditorChange(() => this._update(true), this, this._disposables); + this._update(false); + } + + dispose(): void { + this._disposables.dispose(); + this._delayer.dispose(); + this._combinedEntry?.dispose(); + this._renderDisposables.dispose(); + } + + private _update(clear: boolean): void { + if (clear) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + this._delayer.trigger(() => this._doUpdate()); + } + + private async _doUpdate(): Promise { + const editor = getCodeEditor(this._editorService.activeTextEditorControl); + + this._renderDisposables.clear(); + + // update when editor language changes + editor?.onDidChangeModelLanguage(() => this._update(true), this, this._renderDisposables); + editor?.onDidChangeModelContent(() => this._update(false), this, this._renderDisposables); + const editorModel = editor?.getModel(); + const editorUri = editorModel?.uri; + const existingId = editorModel?.getLanguageId(); + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'textEditors'; + const disableLightbulb = !enabled || editorUri?.scheme !== Schemas.untitled || !existingId; + + if (disableLightbulb || !editorUri) { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } else { + const lang = await this._languageDetectionService.detectLanguage(editorUri); + const skip: Record = { 'jsonc': 'json' }; + const existing = editorModel.getLanguageId(); + if (lang && lang !== existing && skip[existing] !== lang) { + const detectedName = this._languageService.getLanguageName(lang) || lang; + let tooltip = localize('status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(detectLanguageCommandId); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + + const props: IStatusbarEntry = { + name: localize('langDetection.name', "Language Detection"), + ariaLabel: localize('langDetection.aria', "Change to Detected Language: {0}", lang), + tooltip, + command: detectLanguageCommandId, + text: '$(lightbulb-autofix)', + }; + if (!this._combinedEntry) { + this._combinedEntry = this._statusBarService.addEntry(props, LanguageDetectionStatusContribution._id, StatusbarAlignment.RIGHT, { id: 'status.editor.mode', alignment: StatusbarAlignment.RIGHT, compact: true }); + } else { + this._combinedEntry.update(props); + } + } else { + this._combinedEntry?.dispose(); + this._combinedEntry = undefined; + } + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(LanguageDetectionStatusContribution, LifecyclePhase.Restored); + + +registerAction2(class extends Action2 { + + constructor() { + super({ + id: detectLanguageCommandId, + title: localize('detectlang', 'Detect Language from Content'), + f1: true, + precondition: ContextKeyExpr.and(NOTEBOOK_EDITOR_EDITABLE.toNegated(), EditorContextKeys.editorTextFocus), + keybinding: { primary: KeyCode.KeyE | KeyMod.CtrlCmd, weight: KeybindingWeight.WorkbenchContrib } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const languageDetectionService = accessor.get(ILanguageDetectionService); + const editor = getCodeEditor(editorService.activeTextEditorControl); + const notificationService = accessor.get(INotificationService); + const editorUri = editor?.getModel()?.uri; + if (editorUri) { + const lang = await languageDetectionService.detectLanguage(editorUri); + if (lang) { + editor.getModel()?.setMode(lang); + } else { + notificationService.warn(localize('noDetection', "Unable to detect editor language")); + } + } + } +}); diff --git a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts index c0e31f90dd3..2dee3866cbd 100644 --- a/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts +++ b/src/vs/workbench/contrib/localHistory/browser/localHistoryTimeline.ts @@ -8,7 +8,7 @@ import { Emitter } from 'vs/base/common/event'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { InternalTimelineOptions, ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { ITimelineService, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; import { IWorkingCopyHistoryEntry, IWorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; import { URI } from 'vs/base/common/uri'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; @@ -102,7 +102,7 @@ export class LocalHistoryTimeline extends Disposable implements IWorkbenchContri }); } - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise { + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise { const items: TimelineItem[] = []; // Try to convert the provided `uri` into a form that is likely diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 6cbfdc4cd7b..9485c762876 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -14,14 +14,13 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; +import { IOutputChannelRegistry, Extensions as OutputExt, IOutputService } from 'vs/workbench/services/output/common/output'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { LogsDataCleaner } from 'vs/workbench/contrib/logs/common/logsDataCleaner'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { supportsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; import { IProductService } from 'vs/platform/product/common/productService'; import { createCancelablePromise, timeout } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index c47ff0b58ff..68185694831 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -121,7 +121,7 @@ code > div { } .vscode-high-contrast code > div { - background-color: rgb(0, 0, 0); + background-color: var(--vscode-textCodeBlock-background); } .vscode-high-contrast h1 { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts index f1834e4ee96..3747a011ae7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts @@ -3,18 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Delayer } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { CHANGE_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { @@ -50,6 +55,72 @@ class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemP } } +class CellStatusBarLanguageDetectionProvider implements INotebookCellStatusBarItemProvider { + + readonly viewType = '*'; + + private delayer = new Delayer(500); + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @ILanguageService private readonly _languageService: ILanguageService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ILanguageDetectionService private readonly _languageDetectionService: ILanguageDetectionService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { } + + async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise { + return await this.delayer.trigger(async () => { + const doc = this._notebookService.getNotebookTextModel(uri); + const cell = doc?.cells[index]; + if (!cell || token.isCancellationRequested) { + return; + } + + const enablementConfig = this._configurationService.getValue('workbench.editor.languageDetectionHints'); + const enabled = enablementConfig === 'always' || enablementConfig === 'notebookEditors'; + if (!enabled) { + return; + } + + const currentLanguageId = cell.cellKind === CellKind.Markup ? + 'markdown' : + (this._languageService.getLanguageIdByLanguageName(cell.language) || cell.language); + + const kernel = this._notebookKernelService.getMatchingKernel(doc); + const items: INotebookCellStatusBarItem[] = []; + + if (kernel.selected) { + const availableLangs = []; + availableLangs.push(...kernel.selected.supportedLanguages, 'markdown'); + const detectedLanguageId = await this._languageDetectionService.detectLanguage(cell.uri, availableLangs); + + if (detectedLanguageId && currentLanguageId !== detectedLanguageId) { + const detectedName = this._languageService.getLanguageName(detectedLanguageId) || detectedLanguageId; + let tooltip = localize('notebook.cell.status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); + const keybinding = this._keybindingService.lookupKeybinding(DETECT_CELL_LANGUAGE); + const label = keybinding?.getLabel(); + if (label) { + tooltip += ` (${label})`; + } + + items.push({ + text: '$(lightbulb-autofix)', + command: DETECT_CELL_LANGUAGE, + tooltip, + alignment: CellStatusbarAlignment.Right, + priority: -Number.MAX_SAFE_INTEGER + 1 + }); + } + } + + return { items }; + }); + + } +} + class BuiltinCellStatusBarProviders extends Disposable { constructor( @IInstantiationService instantiationService: IInstantiationService, @@ -58,6 +129,7 @@ class BuiltinCellStatusBarProviders extends Disposable { const builtinProviders = [ CellStatusBarLanguagePickerProvider, + CellStatusBarLanguageDetectionProvider, ]; builtinProviders.forEach(p => { this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(p))); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts index 10037c96943..8fbc0999419 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts @@ -27,7 +27,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { IWebview } from 'vs/workbench/contrib/webview/browser/webview'; import { CATEGORIES } from 'vs/workbench/common/actions'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { rendererLogChannelId } from 'vs/workbench/contrib/logs/common/logConstants'; import { ILogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts index cb7ab0f8586..920669194f6 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorStatusBar/editorStatusBar.ts @@ -28,7 +28,7 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { configureKernelIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; @@ -38,7 +38,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat registerAction2(class extends Action2 { constructor() { super({ - id: '_notebook.selectKernel', + id: SELECT_KERNEL_ID, category: NOTEBOOK_ACTIONS_CATEGORY, title: { value: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), original: 'Select Notebook Kernel' }, // precondition: NOTEBOOK_IS_ACTIVE_EDITOR, @@ -182,12 +182,7 @@ registerAction2(class extends Action2 { return res; } const quickPickItems: QuickPickInput[] = []; - if (!all.length) { - quickPickItems.push({ - id: 'install', - label: nls.localize('installKernels', "Install kernels from the marketplace"), - }); - } else { + if (all.length) { // Always display suggested kernels on the top. if (suggestions.length) { quickPickItems.push({ @@ -210,6 +205,14 @@ registerAction2(class extends Action2 { }); } + if (!all.find(item => item.type === NotebookKernelType.Resolved)) { + // there is no resolved kernel, show the install from marketplace + quickPickItems.push({ + id: 'install', + label: nls.localize('installKernels', "Install kernels from the marketplace"), + }); + } + const pick = await quickInputService.pick(quickPickItems, { placeHolder: selected ? nls.localize('prompt.placeholder.change', "Change kernel for '{0}'", labelService.getUriLabel(notebook.uri, { relative: true })) @@ -381,7 +384,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { tooltip: isSuggested ? nls.localize('tooltop', "{0} (suggestion)", tooltip) : tooltip, command: SELECT_KERNEL_ID, }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); @@ -399,7 +402,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { command: SELECT_KERNEL_ID, backgroundColor: { id: 'statusBarItem.prominentBackground' } }, - '_notebook.selectKernel', + SELECT_KERNEL_ID, StatusbarAlignment.RIGHT, 10 )); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts index d0c6379a54b..68d9d0f8dcc 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/apiActions.ts @@ -7,7 +7,7 @@ import * as glob from 'vs/base/common/glob'; import { URI, UriComponents } from 'vs/base/common/uri'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { isDocumentExcludePattern, TransientCellMetadata, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor): { @@ -66,13 +66,13 @@ CommandsRegistry.registerCommand('_resolveNotebookKernels', async (accessor, arg const uri = URI.revive(args.uri as UriComponents); const kernels = notebookKernelService.getMatchingKernel({ uri, viewType: args.viewType }); - return kernels.all.map(provider => ({ + return kernels.all.filter(kernel => kernel.type === NotebookKernelType.Resolved).map((provider) => ({ id: provider.id, label: provider.label, kind: provider.kind, description: provider.description, detail: provider.detail, isPreferred: false, // todo@jrieken,@rebornix - preloads: provider.preloadUris, + preloads: (provider as IResolvedNotebookKernel).preloadUris, })); }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts index 588675f6403..27a1c5b7c21 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/editActions.ts @@ -21,12 +21,13 @@ import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/ import { changeCellToKind, runDeleteAction } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations'; import { CellToolbarOrder, CELL_TITLE_CELL_GROUP_ID, CELL_TITLE_OUTPUT_GROUP_ID, executeNotebookCondition, INotebookActionContext, INotebookCellActionContext, NotebookAction, NotebookCellAction, NOTEBOOK_EDITOR_WIDGET_ACTION_WEIGHT } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { CellEditState, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CHANGE_CELL_LANGUAGE, DETECT_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as icons from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CellEditType, CellKind, ICellEditOperation, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { ILanguageDetectionService } from 'vs/workbench/services/languageDetection/common/languageDetectionWorkerService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; +import { INotificationService } from 'vs/platform/notification/common/notification'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; @@ -437,23 +438,7 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction undefined, undefined, true - ); - } + await setCellToLanguage(languageId, context); } /** @@ -478,3 +463,48 @@ registerAction2(class ChangeCellLanguageAction extends NotebookCellAction { + const languageDetectionService = accessor.get(ILanguageDetectionService); + const notificationService = accessor.get(INotificationService); + const providerLanguages = [...context.notebookEditor.activeKernel?.supportedLanguages ?? []]; + providerLanguages.push('markdown'); + const detection = await languageDetectionService.detectLanguage(context.cell.uri, providerLanguages); + if (detection) { + setCellToLanguage(detection, context); + } else { + notificationService.warn(localize('noDetection', "Unable to detect cell language")); + } + } +}); + +async function setCellToLanguage(languageId: string, context: IChangeCellContext) { + if (languageId === 'markdown' && context.cell?.language !== 'markdown') { + const idx = context.notebookEditor.getCellIndex(context.cell); + await changeCellToKind(CellKind.Markup, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, 'markdown', Mimes.markdown); + const newCell = context.notebookEditor.cellAt(idx); + + if (newCell) { + context.notebookEditor.focusNotebookCell(newCell, 'editor'); + } + } else if (languageId !== 'markdown' && context.cell?.cellKind === CellKind.Markup) { + await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor, ui: true }, languageId); + } else { + const index = context.notebookEditor.textModel.cells.indexOf(context.cell.model); + context.notebookEditor.textModel.applyEdits( + [{ editType: CellEditType.CellLanguage, index, language: languageId }], + true, undefined, () => undefined, undefined, true + ); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index a660892ff1f..e4220b57b14 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -429,7 +429,7 @@ outline: none !important; } -.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { +.monaco-workbench .notebookOverlay.notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar.visible { z-index: var(--z-index-notebook-scrollbar); cursor: default; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 2b793c40961..b96059530d4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -892,5 +892,22 @@ configurationRegistry.registerConfiguration({ enum: ['always', 'never', 'fromEditor'], default: 'fromEditor' }, + [NotebookSetting.outputLineHeight]: { + markdownDescription: nls.localize('notebook.outputLineHeight', "Line height of the output text for notebook cells.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values."), + type: 'number', + default: 22, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontSize]: { + markdownDescription: nls.localize('notebook.outputFontSize', "Font size for the output text for notebook cells. When set to 0 `#editor.fontSize#` is used."), + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontFamily]: { + markdownDescription: nls.localize('notebook.outputFontFamily', "The font family for the output text for notebook cells. When set to empty, the `#editor.fontFamily#` is used."), + type: 'string', + tags: ['notebookLayout'] + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 7153bcfe26c..77a50b8c655 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -11,7 +11,7 @@ import { IEditorContributionDescription } from 'vs/editor/browser/editorExtensio import * as editorCommon from 'vs/editor/common/editorCommon'; import { FontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { FindMatch, IModelDeltaDecoration, IReadonlyTextBuffer, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ITextEditorOptions, ITextResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -31,6 +31,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; +export const DETECT_CELL_LANGUAGE = 'notebook.cell.detectLanguage'; export const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; export const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; export const EXPAND_CELL_OUTPUT_COMMAND_ID = 'notebook.cell.expandCellOutput'; @@ -280,6 +281,7 @@ export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellSelections?: ICellRange[]; readonly isReadOnly?: boolean; readonly viewState?: INotebookEditorViewState; + readonly indexedCellOptions?: { index: number; selection?: IRange }; } export type INotebookEditorContributionCtor = IConstructorSignature; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index aa1bcc7853f..942f49772ca 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -397,7 +397,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._updateForNotebookConfiguration(); } - if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.markupFontSize || e.insertToolbarAlignment) { + if (e.fontFamily) { + this._generateFontInfo(); + } + + if (e.compactView || e.focusIndicator || e.insertToolbarPosition || e.cellToolbarLocation || e.dragAndDropEnabled || e.fontSize || e.outputFontSize || e.markupFontSize || e.fontFamily || e.outputFontFamily || e.insertToolbarAlignment || e.outputLineHeight) { this._styleElement?.remove(); this._createLayoutStyles(); this._webview?.updateOptions({ @@ -1216,8 +1220,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this.viewModel.updateOptions({ isReadOnly: this._readOnly }); // reveal cell if editor options tell to do so - if (options?.cellOptions) { - const cellOptions = options.cellOptions; + const cellOptions = options?.cellOptions ?? this._parseIndexedCellOptions(options); + if (cellOptions) { const cell = this.viewModel.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); if (cell) { this.focusElement(cell); @@ -1270,6 +1274,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD this._onDidChangeOptions.fire(); } + private _parseIndexedCellOptions(options: INotebookEditorOptions | undefined) { + if (options?.indexedCellOptions) { + // convert index based selections + const cell = this.cellAt(options.indexedCellOptions.index); + if (cell) { + return { + resource: cell.uri, + options: { + selection: options.indexedCellOptions.selection, + preserveFocus: false + } + }; + } + } + + return undefined; + } + private _detachModel() { this._localStore.clear(); dispose(this._localCellStateListeners); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts index 000c7b3b72c..2d4fe4cebe0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExecutionServiceImpl.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; @@ -12,10 +14,11 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode import { CellKind, INotebookTextModel, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -export class NotebookExecutionService implements INotebookExecutionService { +export class NotebookExecutionService implements INotebookExecutionService, IDisposable { declare _serviceBrand: undefined; + private _activeProxyKernelExecutionToken: CancellationTokenSource | undefined; constructor( @ICommandService private readonly _commandService: ICommandService, @@ -45,6 +48,29 @@ export class NotebookExecutionService implements INotebookExecutionService { return; } + if (kernel.type === NotebookKernelType.Proxy) { + this._activeProxyKernelExecutionToken?.dispose(true); + const tokenSource = this._activeProxyKernelExecutionToken = new CancellationTokenSource(); + const resolved = await kernel.resolveKernel(notebook.uri); + const kernels = this._notebookKernelService.getMatchingKernel(notebook); + const newlyMatchedKernel = kernels.all.find(k => k.id === resolved); + + if (!newlyMatchedKernel) { + return; + } + + kernel = newlyMatchedKernel; + if (tokenSource.token.isCancellationRequested) { + // execution was cancelled but we still need to update the active kernel + this._notebookKernelService.selectKernelForNotebook(kernel, notebook); + return; + } + } + + if (kernel.type === NotebookKernelType.Proxy) { + return; + } + const executeCells: NotebookCellTextModel[] = []; for (const cell of cellsArr) { const cellExe = this._notebookExecutionStateService.getCellExecution(cell.uri); @@ -75,11 +101,20 @@ export class NotebookExecutionService implements INotebookExecutionService { this._logService.debug(`NotebookExecutionService#cancelNotebookCellHandles ${JSON.stringify(cellsArr)}`); const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(notebook); if (kernel) { - await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + if (kernel.type === NotebookKernelType.Proxy) { + this._activeProxyKernelExecutionToken?.dispose(true); + } else { + await kernel.cancelNotebookCellExecution(notebook.uri, cellsArr); + } + } } async cancelNotebookCells(notebook: INotebookTextModel, cells: Iterable): Promise { this.cancelNotebookCellHandles(notebook, Array.from(cells, cell => cell.handle)); } + + dispose() { + this._activeProxyKernelExecutionToken?.dispose(true); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts index e1e2a982be5..364939e3456 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellContextKeys.ts @@ -5,15 +5,15 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CellEditState, CellFocusMode, ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; +import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellExecutionStateContext, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_RESOURCE, NOTEBOOK_CELL_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; -import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class CellContextKeyPart extends CellPart { private cellContextKeyManager: CellContextKeyManager; @@ -44,6 +44,7 @@ export class CellContextKeyManager extends Disposable { private cellContentCollapsed!: IContextKey; private cellOutputCollapsed!: IContextKey; private cellLineNumbers!: IContextKey<'on' | 'off' | 'inherit'>; + private cellResource!: IContextKey; private markdownEditMode!: IContextKey; @@ -69,6 +70,7 @@ export class CellContextKeyManager extends Disposable { this.cellContentCollapsed = NOTEBOOK_CELL_INPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellOutputCollapsed = NOTEBOOK_CELL_OUTPUT_COLLAPSED.bindTo(this._contextKeyService); this.cellLineNumbers = NOTEBOOK_CELL_LINE_NUMBERS.bindTo(this._contextKeyService); + this.cellResource = NOTEBOOK_CELL_RESOURCE.bindTo(this._contextKeyService); if (element) { this.updateForElement(element); @@ -112,6 +114,7 @@ export class CellContextKeyManager extends Disposable { this.updateForOutputs(); this.cellLineNumbers.set(this.element!.lineNumbers); + this.cellResource.set(this.element!.uri.toString()); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index d4745c02a43..469e7cbef3d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -9,6 +9,7 @@ import { ICellViewModel, INotebookEditorDelegate } from 'vs/workbench/contrib/no import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { NotebookCellInternalMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; export class CellExecutionPart extends CellPart { private kernelDisposables = this._register(new DisposableStore()); @@ -41,7 +42,7 @@ export class CellExecutionPart extends CellPart { } private updateExecutionOrder(internalMetadata: NotebookCellInternalMetadata): void { - if (this._notebookEditor.activeKernel?.implementsExecutionOrder) { + if (this._notebookEditor.activeKernel?.type === NotebookKernelType.Resolved && this._notebookEditor.activeKernel?.implementsExecutionOrder) { const executionOrderLabel = typeof internalMetadata.executionOrder === 'number' ? `[${internalMetadata.executionOrder}]` : '[ ]'; @@ -62,7 +63,8 @@ export class CellExecutionPart extends CellPart { DOM.hide(this._executionOrderLabel); } else { DOM.show(this._executionOrderLabel); - this._executionOrderLabel.style.top = `${element.layoutInfo.editorHeight}px`; + const top = element.layoutInfo.editorHeight - 22 + element.layoutInfo.statusBarHeight; + this._executionOrderLabel.style.top = `${top}px`; } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 7bda661ed4e..a7145c9ffc9 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -37,10 +37,11 @@ import { preloadsScriptStr, RendererMetadata } from 'vs/workbench/contrib/notebo import { transformWebviewThemeVars } from 'vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping'; import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellUri, INotebookRendererInfo, NotebookSetting, RendererMessagingSpec } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernel, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IScopedRendererMessaging } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement, IWebviewService, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewWindowDragMonitor } from 'vs/workbench/contrib/webview/browser/webviewWindowDragMonitor'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { FromWebviewMessage, IAckOutputHeight, IClickedDataUrlMessage, ICodeBlockHighlightRequest, IContentWidgetTopRequest, IControllerPreload, ICreationContent, ICreationRequestMessage, IFindMatch, IMarkupCellInitialization, ToWebviewMessage } from './webviewMessages'; @@ -88,8 +89,11 @@ interface BacklayerWebviewOptions { readonly runGutter: number; readonly dragAndDropEnabled: boolean; readonly fontSize: number; + readonly outputFontSize: number; readonly fontFamily: string; + readonly outputFontFamily: string; readonly markupFontSize: number; + readonly outputLineHeight: number; } export class BackLayerWebView extends Disposable { @@ -204,8 +208,9 @@ export class BackLayerWebView extends Disposable { 'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`, 'notebook-markdown-min-height': `${this.options.previewNodePadding * 2}px`, 'notebook-markup-font-size': typeof this.options.markupFontSize === 'number' && this.options.markupFontSize > 0 ? `${this.options.markupFontSize}px` : `calc(${this.options.fontSize}px * 1.2)`, - 'notebook-cell-output-font-size': `${this.options.fontSize}px`, - 'notebook-cell-output-font-family': this.options.fontFamily, + 'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`, + 'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`, + 'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily, 'notebook-cell-markup-empty-content': nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."), 'notebook-cell-renderer-not-found-error': nls.localize({ key: 'notebook.error.rendererNotFound', @@ -523,6 +528,8 @@ var requirejs = (function() { this.webview.mountTo(this.element); this._register(this.webview); + this._register(new WebviewWindowDragMonitor(() => this.webview)); + this._register(this.webview.onDidClickLink(link => { if (this._disposed) { return; @@ -897,7 +904,7 @@ var requirejs = (function() { } this._preloadsCache.clear(); - if (this._currentKernel) { + if (this._currentKernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(this._currentKernel); } @@ -1394,14 +1401,14 @@ var requirejs = (function() { const previousKernel = this._currentKernel; this._currentKernel = kernel; - if (previousKernel && previousKernel.preloadUris.length > 0) { + if (previousKernel?.type === NotebookKernelType.Resolved && previousKernel.preloadUris.length > 0) { this.webview?.reload(); // preloads will be restored after reload - } else if (kernel) { + } else if (kernel?.type === NotebookKernelType.Resolved) { this._updatePreloadsFromKernel(kernel); } } - private _updatePreloadsFromKernel(kernel: INotebookKernel) { + private _updatePreloadsFromKernel(kernel: IResolvedNotebookKernel) { const resources: IControllerPreload[] = []; for (const preload of kernel.preloadUris) { const uri = this.environmentService.isExtensionDevelopment && (preload.scheme === 'http' || preload.scheme === 'https') @@ -1427,7 +1434,7 @@ var requirejs = (function() { const mixedResourceRoots = [ ...(this.localResourceRootsCache || []), - ...(this._currentKernel ? [this._currentKernel.localResourceRoot] : []), + ...(this._currentKernel?.type === NotebookKernelType.Resolved ? [this._currentKernel.localResourceRoot] : []), ]; this.webview.localResourcesRoot = mixedResourceRoots; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts index a90477db542..b1cf70a4946 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorWidgetContextKeys.ts @@ -8,7 +8,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { ICellViewModel, INotebookEditorDelegate, KERNEL_EXTENSIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NOTEBOOK_CELL_TOOLBAR_LOCATION, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_HAS_RUNNING_CELL, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_KERNEL_SELECTED, NOTEBOOK_MISSING_KERNEL_EXTENSION, NOTEBOOK_USE_CONSOLIDATED_OUTPUT_BUTTON, NOTEBOOK_VIEW_TYPE } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export class NotebookEditorContextKeys { @@ -148,7 +148,7 @@ export class NotebookEditorContextKeys { const { selected, all } = this._notebookKernelService.getMatchingKernel(this._editor.textModel); this._notebookKernelCount.set(all.length); - this._interruptibleKernel.set(selected?.implementsInterrupt ?? false); + this._interruptibleKernel.set((selected?.type === NotebookKernelType.Resolved && selected.implementsInterrupt) ?? false); this._notebookKernelSelected.set(Boolean(selected)); this._notebookKernel.set(selected?.id ?? ''); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts index dc9a494193e..ed9098ad49d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelActionViewItem.ts @@ -6,10 +6,11 @@ import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; -import { INotebookKernelMatchResult, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelMatchResult, INotebookKernelService, NotebookKernelType, ProxyKernelState } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { Event } from 'vs/base/common/event'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -17,6 +18,7 @@ import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookB export class NotebooKernelActionViewItem extends ActionViewItem { private _kernelLabel?: HTMLAnchorElement; + private _kernelDisposable: DisposableStore; constructor( actualAction: IAction, @@ -31,6 +33,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { this._register(_editor.onDidChangeModel(this._update, this)); this._register(_notebookKernelService.onDidChangeNotebookAffinity(this._update, this)); this._register(_notebookKernelService.onDidChangeSelectedNotebooks(this._update, this)); + this._kernelDisposable = this._register(new DisposableStore()); } override render(container: HTMLElement): void { @@ -63,9 +66,9 @@ export class NotebooKernelActionViewItem extends ActionViewItem { } private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { - + this._kernelDisposable.clear(); this._action.enabled = true; - const selectedOrSuggested = info.selected ?? (info.all.length === 1 && info.suggestions.length === 1 ? info.suggestions[0] : undefined); + const selectedOrSuggested = info.selected ?? ((info.all.length === 1 && info.suggestions.length === 1 && info.suggestions[0].type === NotebookKernelType.Resolved) ? info.suggestions[0] : undefined); if (selectedOrSuggested) { // selected or suggested kernel this._action.label = selectedOrSuggested.label; @@ -74,6 +77,23 @@ export class NotebooKernelActionViewItem extends ActionViewItem { // special UI for selected kernel? } + if (selectedOrSuggested.type === NotebookKernelType.Proxy) { + if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { + this._action.label = localize('initializing', "Initializing..."); + } else { + this._action.label = selectedOrSuggested.label; + } + + this._kernelDisposable.add(selectedOrSuggested.onDidChange(e => { + if (e.connectionState) { + if (selectedOrSuggested.connectionState === ProxyKernelState.Initializing) { + this._action.label = localize('initializing', "Initializing..."); + } else { + this._action.label = selectedOrSuggested.label; + } + } + })); + } } else { // many kernels or no kernels this._action.label = localize('select', "Select Kernel"); diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index b93c8a7449f..5f70d558da1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -913,7 +913,10 @@ export const NotebookSetting = { textOutputLineLimit: 'notebook.output.textLineLimit', globalToolbarShowLabel: 'notebook.globalToolbarShowLabel', markupFontSize: 'notebook.markup.fontSize', - interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode' + interactiveWindowCollapseCodeCells: 'interactiveWindow.collapseCellInputCode', + outputLineHeight: 'notebook.outputLineHeight', + outputFontSize: 'notebook.outputFontSize', + outputFontFamily: 'notebook.outputFontFamily' } as const; export const enum CellStatusbarAlignment { diff --git a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts index fd2279cba63..689f9ca995e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookContextKeys.ts @@ -38,6 +38,8 @@ export const NOTEBOOK_CELL_EXECUTING = new RawContextKey('notebookCellE export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); export const NOTEBOOK_CELL_INPUT_COLLAPSED = new RawContextKey('notebookCellInputIsCollapsed', false); export const NOTEBOOK_CELL_OUTPUT_COLLAPSED = new RawContextKey('notebookCellOutputIsCollapsed', false); +export const NOTEBOOK_CELL_RESOURCE = new RawContextKey('notebookCellResource', ''); + // Kernels export const NOTEBOOK_KERNEL = new RawContextKey('notebookKernel', undefined); export const NOTEBOOK_KERNEL_COUNT = new RawContextKey('notebookKernelCount', 0); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index cffd2b0362d..6e28efdd088 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -102,6 +102,10 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } } + if (!(capabilities & EditorInputCapabilities.Readonly)) { + capabilities |= EditorInputCapabilities.CanDropIntoEditor; + } + return capabilities; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 6610fe7177d..4f5304600b0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -31,8 +31,13 @@ export interface INotebookKernelChangeEvent { hasExecutionOrder?: true; } -export interface INotebookKernel { +export const enum NotebookKernelType { + Resolved, + Proxy = 1 +} +export interface IResolvedNotebookKernel { + readonly type: NotebookKernelType.Resolved; readonly id: string; readonly viewType: string; readonly onDidChange: Event>; @@ -54,6 +59,34 @@ export interface INotebookKernel { cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; } +export const enum ProxyKernelState { + Disconnected = 1, + Connected = 2, + Initializing = 3 +} + +export interface INotebookProxyKernelChangeEvent extends INotebookKernelChangeEvent { + connectionState?: true; +} + +export interface INotebookProxyKernel { + readonly type: NotebookKernelType.Proxy; + readonly id: string; + readonly viewType: string; + readonly extension: ExtensionIdentifier; + readonly preloadProvides: string[]; + readonly onDidChange: Event>; + label: string; + description?: string; + detail?: string; + kind?: string; + supportedLanguages: string[]; + connectionState: ProxyKernelState; + resolveKernel(uri: URI): Promise; +} + +export type INotebookKernel = IResolvedNotebookKernel | INotebookProxyKernel; + export interface INotebookTextModelLike { uri: URI; viewType: string } export const INotebookKernelService = createDecorator('INotebookKernelService'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts index 9564a563def..426871f84fa 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOptions.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOptions.ts @@ -62,6 +62,9 @@ export interface NotebookLayoutConfiguration { showFoldingControls: 'always' | 'mouseover'; dragAndDropEnabled: boolean; fontSize: number; + outputFontSize: number; + outputFontFamily: string; + outputLineHeight: number; markupFontSize: number; focusIndicatorLeftMargin: number; editorOptionsCustomizations: any | undefined; @@ -84,9 +87,13 @@ export interface NotebookOptionsChangeEvent { readonly consolidatedRunButton?: boolean; readonly dragAndDropEnabled?: boolean; readonly fontSize?: boolean; + readonly outputFontSize?: boolean; readonly markupFontSize?: boolean; + readonly fontFamily?: boolean; + readonly outputFontFamily?: boolean; readonly editorOptionsCustomizations?: boolean; readonly interactiveWindowCollapseCodeCells?: boolean; + readonly outputLineHeight?: boolean; } const defaultConfigConstants = Object.freeze({ @@ -134,9 +141,12 @@ export class NotebookOptions extends Disposable { const showFoldingControls = this._computeShowFoldingControlsOption(); // const { bottomToolbarGap, bottomToolbarHeight } = this._computeBottomToolbarDimensions(compactView, insertToolbarPosition, insertToolbarAlignment); const fontSize = this.configurationService.getValue('editor.fontSize'); + const outputFontSize = this.configurationService.getValue(NotebookSetting.outputFontSize); + const outputFontFamily = this.configurationService.getValue(NotebookSetting.outputFontFamily); const markupFontSize = this.configurationService.getValue(NotebookSetting.markupFontSize); const editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells: InteractiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = this._computeOutputLineHeight(); this._layoutConfiguration = { ...(compactView ? compactConfigConstants : defaultConfigConstants), @@ -166,6 +176,9 @@ export class NotebookOptions extends Disposable { insertToolbarAlignment, showFoldingControls, fontSize, + outputFontSize, + outputFontFamily, + outputLineHeight, markupFontSize, editorOptionsCustomizations, focusIndicatorGap: 3, @@ -185,6 +198,29 @@ export class NotebookOptions extends Disposable { })); } + private _computeOutputLineHeight(): number { + const minimumLineHeight = 8; + let lineHeight = this.configurationService.getValue(NotebookSetting.outputLineHeight); + + if (lineHeight < minimumLineHeight) { + // Values too small to be line heights in pixels are in ems. + let fontSize = this.configurationService.getValue(NotebookSetting.outputFontSize); + if (fontSize === 0) { + fontSize = this.configurationService.getValue('editor.fontSize'); + } + + lineHeight = lineHeight * fontSize; + } + + // Enforce integer, minimum constraints + lineHeight = Math.round(lineHeight); + if (lineHeight < minimumLineHeight) { + lineHeight = minimumLineHeight; + } + + return lineHeight; + } + private _updateConfiguration(e: IConfigurationChangeEvent) { const cellStatusBarVisibility = e.affectsConfiguration(NotebookSetting.showCellStatusBar); const cellToolbarLocation = e.affectsConfiguration(NotebookSetting.cellToolbarLocation); @@ -199,9 +235,13 @@ export class NotebookOptions extends Disposable { const showFoldingControls = e.affectsConfiguration(NotebookSetting.showFoldingControls); const dragAndDropEnabled = e.affectsConfiguration(NotebookSetting.dragAndDropEnabled); const fontSize = e.affectsConfiguration('editor.fontSize'); + const outputFontSize = e.affectsConfiguration(NotebookSetting.outputFontSize); const markupFontSize = e.affectsConfiguration(NotebookSetting.markupFontSize); + const fontFamily = e.affectsConfiguration('editor.fontFamily'); + const outputFontFamily = e.affectsConfiguration(NotebookSetting.outputFontFamily); const editorOptionsCustomizations = e.affectsConfiguration(NotebookSetting.cellEditorOptionsCustomizations); const interactiveWindowCollapseCodeCells = e.affectsConfiguration(NotebookSetting.interactiveWindowCollapseCodeCells); + const outputLineHeight = e.affectsConfiguration(NotebookSetting.outputLineHeight); if ( !cellStatusBarVisibility @@ -217,9 +257,13 @@ export class NotebookOptions extends Disposable { && !showFoldingControls && !dragAndDropEnabled && !fontSize + && !outputFontSize && !markupFontSize + && !fontFamily + && !outputFontFamily && !editorOptionsCustomizations - && !interactiveWindowCollapseCodeCells) { + && !interactiveWindowCollapseCodeCells + && !outputLineHeight) { return; } @@ -281,10 +325,18 @@ export class NotebookOptions extends Disposable { configuration.fontSize = this.configurationService.getValue('editor.fontSize'); } + if (outputFontSize) { + configuration.outputFontSize = this.configurationService.getValue(NotebookSetting.outputFontSize) ?? configuration.fontSize; + } + if (markupFontSize) { configuration.markupFontSize = this.configurationService.getValue(NotebookSetting.markupFontSize); } + if (outputFontFamily) { + configuration.outputFontFamily = this.configurationService.getValue(NotebookSetting.outputFontFamily); + } + if (editorOptionsCustomizations) { configuration.editorOptionsCustomizations = this.configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations); } @@ -293,6 +345,10 @@ export class NotebookOptions extends Disposable { configuration.interactiveWindowCollapseCodeCells = this.configurationService.getValue(NotebookSetting.interactiveWindowCollapseCodeCells); } + if (outputLineHeight || fontSize || outputFontSize) { + configuration.outputLineHeight = this._computeOutputLineHeight(); + } + this._layoutConfiguration = Object.freeze(configuration); // trigger event @@ -310,9 +366,13 @@ export class NotebookOptions extends Disposable { consolidatedRunButton, dragAndDropEnabled, fontSize, + outputFontSize, markupFontSize, + fontFamily, + outputFontFamily, editorOptionsCustomizations, - interactiveWindowCollapseCodeCells + interactiveWindowCollapseCodeCells, + outputLineHeight }); } @@ -502,7 +562,10 @@ export class NotebookOptions extends Disposable { runGutter: this._layoutConfiguration.cellRunGutter, dragAndDropEnabled: this._layoutConfiguration.dragAndDropEnabled, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } @@ -517,7 +580,10 @@ export class NotebookOptions extends Disposable { runGutter: 0, dragAndDropEnabled: false, fontSize: this._layoutConfiguration.fontSize, + outputFontSize: this._layoutConfiguration.outputFontSize, + outputFontFamily: this._layoutConfiguration.outputFontFamily, markupFontSize: this._layoutConfiguration.markupFontSize, + outputLineHeight: this._layoutConfiguration.outputLineHeight, }; } diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts index 6f65268830f..8d3bff7a83a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionService.test.ts @@ -20,7 +20,7 @@ import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CellKind, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelService, ISelectedNotebooksChangeEvent } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, ISelectedNotebooksChangeEvent, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -166,7 +166,8 @@ suite('NotebookExecutionService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = 'test'; label: string = ''; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts index 887a4a05443..e5b7f84b8bf 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookExecutionStateService.test.ts @@ -21,7 +21,7 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { CellEditType, CellKind, CellUri, IOutputDto, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; @@ -170,7 +170,8 @@ suite('NotebookExecutionStateService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = 'test'; label: string = ''; viewType = '*'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts index b1ebabc1db2..62d6afecb8b 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelService.test.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelService, IResolvedNotebookKernel, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { mock } from 'vs/base/test/common/mock'; @@ -159,7 +159,8 @@ suite('NotebookKernelService', () => { }); }); -class TestNotebookKernel implements INotebookKernel { +class TestNotebookKernel implements IResolvedNotebookKernel { + type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; id: string = Math.random() + 'kernel'; label: string = 'test-label'; viewType = '*'; diff --git a/src/vs/workbench/contrib/output/browser/logViewer.ts b/src/vs/workbench/contrib/output/browser/logViewer.ts index a542e7dbe91..3c78aa114d1 100644 --- a/src/vs/workbench/contrib/output/browser/logViewer.ts +++ b/src/vs/workbench/contrib/output/browser/logViewer.ts @@ -15,8 +15,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { URI } from 'vs/base/common/uri'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { LOG_SCHEME } from 'vs/workbench/contrib/output/common/output'; -import { IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; +import { LOG_SCHEME, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index b6dbbb8caf7..f9e4814794e 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -12,7 +12,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor'; import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; @@ -25,7 +25,6 @@ import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensio import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined } from 'vs/base/common/types'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index eb346086655..ca06b37e21f 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -8,7 +8,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IModelService } from 'vs/editor/common/services/model'; import { ILink } from 'vs/editor/common/languages'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, LOG_MODE_ID } from 'vs/workbench/services/output/common/output'; import { MonacoWebWorker, createWebWorker } from 'vs/editor/browser/services/webWorker'; import { ICreateData, OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index 4901c9f23ce..dffb7046911 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -9,8 +9,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; -import { IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; +import { IOutputChannel, IOutputService, OUTPUT_VIEW_ID, OUTPUT_SCHEME, LOG_SCHEME, LOG_MIME, OUTPUT_MIME, OutputChannelUpdateMode, IOutputChannelDescriptor, Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; import { OutputLinkProvider } from 'vs/workbench/contrib/output/browser/outputLinkProvider'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index ee9c7eadf5d..545d41fdcee 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; -import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK, IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -27,7 +27,6 @@ import { IContextMenuService, IContextViewService } from 'vs/platform/contextvie import { IViewDescriptorService } from 'vs/workbench/common/views'; import { TextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; import { Registry } from 'vs/platform/registry/common/platform'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; diff --git a/src/vs/workbench/contrib/output/common/outputChannelModel.ts b/src/vs/workbench/contrib/output/common/outputChannelModel.ts index cb24f441e21..4c01e67797c 100644 --- a/src/vs/workbench/contrib/output/common/outputChannelModel.ts +++ b/src/vs/workbench/contrib/output/common/outputChannelModel.ts @@ -21,7 +21,7 @@ import { Range } from 'vs/editor/common/core/range'; import { VSBuffer } from 'vs/base/common/buffer'; import { ILogger, ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { OutputChannelUpdateMode } from 'vs/workbench/contrib/output/common/output'; +import { OutputChannelUpdateMode } from 'vs/workbench/services/output/common/output'; export interface IOutputChannelModel extends IDisposable { readonly onDispose: Event; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 7145fe94aa9..a09f097b305 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -39,7 +39,7 @@ .settings-editor > .settings-header > .search-container > .settings-count-widget { position: absolute; - right: 35px; + right: 46px; top: 0px; margin: 4px 0px; } @@ -55,7 +55,7 @@ top: 0; right: 0; height: 100%; - width: 30px; + width: 43px; } .settings-editor > .settings-header > .search-container > .settings-clear-widget .action-label { diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 42365a36d4f..b5230a5af22 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -34,7 +34,7 @@ import { ConfigureLanguageBasedSettingsAction } from 'vs/workbench/contrib/prefe import { SettingsEditorContribution } from 'vs/workbench/contrib/preferences/browser/preferencesEditor'; import { preferencesOpenSettingsIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { SettingsEditor2, SettingsFocusContext } from 'vs/workbench/contrib/preferences/browser/settingsEditor2'; -import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { PreferencesContribution } from 'vs/workbench/contrib/preferences/common/preferencesContribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -54,7 +54,6 @@ const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingContr const SETTINGS_EDITOR_COMMAND_FOCUS_UP = 'settings.action.focusLevelUp'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; -const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; const SETTINGS_EDITOR_COMMAND_FILTER_ONLINE = 'settings.filterByOnline'; const SETTINGS_EDITOR_COMMAND_FILTER_TELEMETRY = 'settings.filterByTelemetry'; const SETTINGS_EDITOR_COMMAND_FILTER_UNTRUSTED = 'settings.filterUntrusted'; @@ -393,36 +392,15 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon return accessor.get(IPreferencesService).openFolderSettings({ folderUri: resource }); } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED, - title: { value: nls.localize('filterModifiedLabel', "Show modified settings"), original: 'Show modified settings' }, - menu: { - id: MenuId.EditorTitle, - group: '1_filter', - order: 1, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) - } - }); - } - run(accessor: ServicesAccessor, resource: URI) { - const editorPane = accessor.get(IEditorService).activeEditorPane; - if (editorPane instanceof SettingsEditor2) { - editorPane.focusSearch(`@${MODIFIED_SETTING_TAG}`); - } - } - }); registerAction2(class extends Action2 { constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: { value: nls.localize('filterOnlineServicesLabel', "Show settings for online services"), original: 'Show settings for online services' }, + title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings"), menu: { - id: MenuId.EditorTitle, - group: '1_filter', + id: MenuId.MenubarPreferencesMenu, + group: '1_settings', order: 2, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR.toNegated()) } }); } @@ -435,22 +413,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } } }); - MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { - group: '1_settings', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize({ key: 'miOpenOnlineSettings', comment: ['&& denotes a mnemonic'] }, "&&Online Services Settings") - }, - order: 2 - }); - MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { - group: '2_configuration', - command: { - id: SETTINGS_EDITOR_COMMAND_FILTER_ONLINE, - title: nls.localize('onlineServices', "Online Services Settings") - }, - order: 2 - }); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts index 6692bf47bd4..0c31b455aaf 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts @@ -25,4 +25,5 @@ export const settingsRemoveIcon = registerIcon('settings-remove', Codicon.close, export const settingsDiscardIcon = registerIcon('settings-discard', Codicon.discard, localize('preferencesDiscardIcon', 'Icon for the discard action in the Settings UI.')); export const preferencesClearInputIcon = registerIcon('preferences-clear-input', Codicon.clearAll, localize('preferencesClearInput', 'Icon for clear input in the Settings and keybinding UI.')); +export const preferencesFilterIcon = registerIcon('preferences-filter', Codicon.filter, localize('settingsFilter', 'Icon for the button that suggests filters for the Settings UI.')); export const preferencesOpenSettingsIcon = registerIcon('preferences-open-settings', Codicon.goToFile, localize('preferencesOpenSettings', 'Icon for open settings commands.')); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 780ebf0a742..3afedd34c39 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -43,14 +43,14 @@ import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/brow import { AbstractSettingRenderer, HeightChangeParams, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveConfiguredUntrustedSettings, createTocTreeForExtensionSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; -import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, ENABLE_LANGUAGE_FILTER, EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, ID_SETTING_TAG, IPreferencesSearchService, ISearchProvider, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, WORKSPACE_TRUST_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; import { settingsHeaderBorder, settingsSashBorder, settingsTextInputBorder } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISettingsEditorModel, ISettingsEditorOptions, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { Settings2EditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; import { IUserDataSyncWorkbenchService } from 'vs/workbench/services/userDataSync/common/userDataSync'; -import { preferencesClearInputIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; +import { preferencesClearInputIcon, preferencesFilterIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration'; @@ -58,6 +58,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Color } from 'vs/base/common/color'; import { ILanguageService } from 'vs/editor/common/languages/language'; +import { SettingsSearchFilterDropdownMenuActionViewItem } from 'vs/workbench/contrib/preferences/browser/settingsSearchMenu'; export const enum SettingsFocusContext { Search, @@ -499,11 +500,11 @@ export class SettingsEditor2 extends EditorPane { clearSearchFilters(): void { let query = this.searchWidget.getValue(); - SettingsEditor2.SUGGESTIONS.forEach(suggestion => { - query = query.replace(suggestion, ''); + const splitQuery = query.split(' ').filter(word => { + return word.length && !SettingsEditor2.SUGGESTIONS.some(suggestion => word.startsWith(suggestion)); }); - this.searchWidget.setValue(query.trim()); + this.searchWidget.setValue(splitQuery.join(' ')); } private updateInputAriaLabel() { @@ -525,7 +526,7 @@ export class SettingsEditor2 extends EditorPane { const searchContainer = DOM.append(this.headerContainer, $('.search-container')); const clearInputAction = new Action(SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, localize('clearInput', "Clear Settings Search Input"), ThemeIcon.asClassName(preferencesClearInputIcon), false, async () => this.clearSearchResults()); - + const filterAction = new Action(SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS, localize('filterInput', "Filter Settings"), ThemeIcon.asClassName(preferencesFilterIcon)); this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { triggerCharacters: ['@', ':'], provideResults: (query: string) => { @@ -533,9 +534,10 @@ export class SettingsEditor2 extends EditorPane { // for the ':' trigger, only return suggestions if there was a '@' before it in the same word. const queryParts = query.split(/\s/g); if (queryParts[queryParts.length - 1].startsWith(`@${LANGUAGE_SETTING_TAG}`)) { - return this.languageService.getRegisteredLanguageIds().map(languageId => { + const sortedLanguages = this.languageService.getRegisteredLanguageIds().map(languageId => { return `@${LANGUAGE_SETTING_TAG}${languageId} `; }).sort(); + return sortedLanguages.filter(langFilter => !query.includes(langFilter)); } else if (queryParts[queryParts.length - 1].startsWith('@')) { return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' '); } @@ -603,10 +605,15 @@ export class SettingsEditor2 extends EditorPane { const actionBar = this._register(new ActionBar(this.controlsElement, { animated: false, - actionViewItemProvider: (_action) => { return undefined; } + actionViewItemProvider: (action) => { + if (action.id === filterAction.id) { + return this.instantiationService.createInstance(SettingsSearchFilterDropdownMenuActionViewItem, action, this.actionRunner, this.searchWidget); + } + return undefined; + } })); - actionBar.push([clearInputAction], { label: false, icon: true }); + actionBar.push([clearInputAction, filterAction], { label: false, icon: true }); } private onDidSettingsTargetChange(target: SettingsTarget): void { @@ -831,7 +838,11 @@ export class SettingsEditor2 extends EditorPane { } })); this._register(this.settingRenderers.onApplyLanguageFilter((lang: string) => { - this.focusSearch(`@${LANGUAGE_SETTING_TAG}${lang}`); + if (this.searchWidget) { + // Prepend the language filter to the query. + const newQuery = `@${LANGUAGE_SETTING_TAG}${lang} ${this.searchWidget.getValue().trimStart()}`; + this.focusSearch(newQuery, false); + } })); this.settingsTree = this._register(this.instantiationService.createInstance(SettingsTree, diff --git a/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts new file mode 100644 index 00000000000..333d179f818 --- /dev/null +++ b/src/vs/workbench/contrib/preferences/browser/settingsSearchMenu.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; +import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem'; +import { IAction, IActionRunner } from 'vs/base/common/actions'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { localize } from 'vs/nls'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; +import { EXTENSION_SETTING_TAG, FEATURE_SETTING_TAG, GENERAL_TAG_SETTING_TAG, LANGUAGE_SETTING_TAG, MODIFIED_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; + +export class SettingsSearchFilterDropdownMenuActionViewItem extends DropdownMenuActionViewItem { + private readonly suggestController: SuggestController | null; + + constructor( + action: IAction, + actionRunner: IActionRunner | undefined, + private readonly searchWidget: SuggestEnabledInput, + @IContextMenuService contextMenuService: IContextMenuService + ) { + super(action, + { getActions: () => this.getActions() }, + contextMenuService, + { + actionRunner, + classNames: action.class, + anchorAlignmentProvider: () => AnchorAlignment.RIGHT, + menuAsChild: true + } + ); + + this.suggestController = SuggestController.get(this.searchWidget.inputWidget); + } + + override render(container: HTMLElement): void { + super.render(container); + } + + private doSearchWidgetAction(queryToAppend: string, triggerSuggest: boolean) { + this.searchWidget.setValue(this.searchWidget.getValue().trimEnd() + ' ' + queryToAppend); + this.searchWidget.focus(); + if (triggerSuggest && this.suggestController) { + this.suggestController.triggerSuggest(); + } + } + + /** + * The created action appends a query to the search widget search string. It optionally triggers suggestions. + */ + private createAction(id: string, label: string, tooltip: string, queryToAppend: string, triggerSuggest: boolean): IAction { + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: false, + run: () => { this.doSearchWidgetAction(queryToAppend, triggerSuggest); }, + dispose: () => { } + }; + } + + /** + * The created action appends a query to the search widget search string, if the query does not exist. + * Otherwise, it removes the query from the search widget search string. + * The action does not trigger suggestions after adding or removing the query. + */ + private createToggleAction(id: string, label: string, tooltip: string, queryToAppend: string): IAction { + const splitCurrentQuery = this.searchWidget.getValue().split(' '); + const queryContainsQueryToAppend = splitCurrentQuery.includes(queryToAppend); + return { + id, + label, + tooltip, + class: undefined, + enabled: true, + checked: queryContainsQueryToAppend, + run: () => { + if (!queryContainsQueryToAppend) { + const trimmedCurrentQuery = this.searchWidget.getValue().trimEnd(); + const newQuery = trimmedCurrentQuery ? trimmedCurrentQuery + ' ' + queryToAppend : queryToAppend; + this.searchWidget.setValue(newQuery); + } else { + const queryWithRemovedTags = this.searchWidget.getValue().split(' ') + .filter(word => word !== queryToAppend).join(' '); + this.searchWidget.setValue(queryWithRemovedTags); + } + this.searchWidget.focus(); + }, + dispose: () => { } + }; + } + + getActions(): IAction[] { + return [ + this.createToggleAction( + 'modifiedSettingsSearch', + localize('modifiedSettingsSearch', "Modified"), + localize('modifiedSettingsSearchTooltip', "Add or remove modified settings filter"), + `@${MODIFIED_SETTING_TAG}` + ), + this.createAction( + 'extSettingsSearch', + localize('extSettingsSearch', "Extension ID..."), + localize('extSettingsSearchTooltip', "Add extension ID filter"), + `@${EXTENSION_SETTING_TAG}`, + false + ), + this.createAction( + 'featuresSettingsSearch', + localize('featureSettingsSearch', "Feature..."), + localize('featureSettingsSearchTooltip', "Add feature filter"), + `@${FEATURE_SETTING_TAG}`, + true + ), + this.createAction( + 'tagSettingsSearch', + localize('tagSettingsSearch', "Tag..."), + localize('tagSettingsSearchTooltip', "Add tag filter"), + `@${GENERAL_TAG_SETTING_TAG}`, + true + ), + this.createAction( + 'langSettingsSearch', + localize('langSettingsSearch', "Language..."), + localize('langSettingsSearchTooltip', "Add language ID filter"), + `@${LANGUAGE_SETTING_TAG}`, + true + ), + this.createToggleAction( + 'onlineSettingsSearch', + localize('onlineSettingsSearch', "Online services"), + localize('onlineSettingsSearchTooltip', "Show settings for online services"), + '@tag:usesOnlineServices' + ) + ]; + } +} diff --git a/src/vs/workbench/contrib/preferences/common/preferences.ts b/src/vs/workbench/contrib/preferences/common/preferences.ts index ec3a14f1bf4..6138098d36f 100644 --- a/src/vs/workbench/contrib/preferences/common/preferences.ts +++ b/src/vs/workbench/contrib/preferences/common/preferences.ts @@ -42,6 +42,7 @@ export interface ISearchProvider { export const SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS = 'settings.action.clearSearchResults'; export const SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU = 'settings.action.showContextMenu'; +export const SETTINGS_EDITOR_COMMAND_SUGGEST_FILTERS = 'settings.action.suggestFilters'; export const CONTEXT_SETTINGS_EDITOR = new RawContextKey('inSettingsEditor', false); export const CONTEXT_SETTINGS_JSON_EDITOR = new RawContextKey('inSettingsJSONEditor', false); @@ -76,6 +77,7 @@ export const EXTENSION_SETTING_TAG = 'ext:'; export const FEATURE_SETTING_TAG = 'feature:'; export const ID_SETTING_TAG = 'id:'; export const LANGUAGE_SETTING_TAG = 'lang:'; +export const GENERAL_TAG_SETTING_TAG = 'tag:'; export const WORKSPACE_TRUST_SETTING_TAG = 'workspaceTrust'; export const REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG = 'requireTrustedWorkspace'; export const KEYBOARD_LAYOUT_OPEN_PICKER = 'workbench.action.openKeyboardLayoutPicker'; diff --git a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts index 6d2258b96f1..755c6fa307c 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @@ -188,9 +188,23 @@ export class ClearCommandHistoryAction extends Action2 { async run(accessor: ServicesAccessor): Promise { const configurationService = accessor.get(IConfigurationService); const storageService = accessor.get(IStorageService); + const dialogService = accessor.get(IDialogService); const commandHistoryLength = CommandsHistory.getConfiguredCommandHistoryLength(configurationService); if (commandHistoryLength > 0) { + + // Ask for confirmation + const { confirmed } = await dialogService.confirm({ + message: localize('confirmClearMessage', "Do you want to clear the history of recently used commands?"), + detail: localize('confirmClearDetail', "This action is irreversible!"), + primaryButton: localize({ key: 'clearButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Clear"), + type: 'warning' + }); + + if (!confirmed) { + return; + } + CommandsHistory.clearHistory(configurationService, storageService); } } diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts index d1a3d7680d5..4a278af2146 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IQuickPickSeparator, IQuickInputService, ItemActivation } from 'vs/platform/quickinput/common/quickInput'; import { IPickerQuickAccessItem, PickerQuickAccessProvider } from 'vs/platform/quickinput/browser/pickerQuickAccess'; import { IViewDescriptorService, IViewsService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { PaneCompositeDescriptor } from 'vs/workbench/browser/panecomposite'; diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 2b5f202e350..89b85c7ecae 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -236,7 +236,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis localize('scm.defaultViewSortKey.path', "Sort the repository changes by path."), localize('scm.defaultViewSortKey.status', "Sort the repository changes by Source Control status.") ], - description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository sort mode."), + description: localize('scm.defaultViewSortKey', "Controls the default Source Control repository changes sort order when viewed as a list."), default: 'path' }, 'scm.autoReveal': { @@ -259,6 +259,17 @@ Registry.as(ConfigurationExtensions.Configuration).regis markdownDescription: localize('alwaysShowRepository', "Controls whether repositories should always be visible in the Source Control view."), default: false }, + 'scm.repositories.sortOrder': { + type: 'string', + enum: ['discovery time', 'name', 'path'], + enumDescriptions: [ + localize('scm.repositoriesSortOrder.discoveryTime', "Repositories in the Source Control Repositories view are sorted by discovery time. Repositories in the Source Control view are sorted in the order that they were selected."), + localize('scm.repositoriesSortOrder.name', "Repositories in the Source Control Repositories and Source Control views are sorted by repository name."), + localize('scm.repositoriesSortOrder.path', "Repositories in the Source Control Repositories and Source Control views are sorted by repository path.") + ], + description: localize('repositoriesSortOrder', "Controls the sort order of the repositories in the source control repositories view."), + default: 'discovery time' + }, 'scm.repositories.visible': { type: 'number', description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."), diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 79cce93a380..3eb672f06c5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,7 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -84,6 +84,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; import { Button, ButtonWithDescription } from 'vs/base/browser/ui/button/button'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -944,7 +945,7 @@ class RepositoryVisibilityAction extends Action2 { f1: false, precondition: ContextKeyExpr.or(ContextKeys.RepositoryVisibilityCount.notEqualsTo(1), ContextKeys.RepositoryVisibility(repository).isEqualTo(false)), toggled: ContextKeys.RepositoryVisibility(repository).isEqualTo(true), - menu: { id: Menus.Repositories } + menu: { id: Menus.Repositories, group: '0_repositories' } }); this.repository = repository; } @@ -1069,9 +1070,14 @@ class ViewModel { } } + // Update sort key based on view mode + this.sortKey = this.getViewModelSortKey(); + this.refresh(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); + + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); } get sortKey(): ViewModelSortKey { return this._sortKey; } @@ -1085,6 +1091,10 @@ class ViewModel { this.refresh(); this._onDidChangeSortKey.fire(sortKey); this.sortKeyContextKey.set(sortKey); + + if (this._mode === ViewModelMode.List) { + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); + } } private _treeViewStateIsStale = false; @@ -1113,23 +1123,37 @@ class ViewModel { private scmProviderRootUriContextKey: IContextKey; private scmProviderHasRootUriContextKey: IContextKey; + private _mode: ViewModelMode; + private _sortKey: ViewModelSortKey; + private _treeViewState: ITreeViewState | undefined; + constructor( private tree: WorkbenchCompressibleObjectTree, private inputRenderer: InputRenderer, - private _mode: ViewModelMode, - private _sortKey: ViewModelSortKey, - private _treeViewState: ITreeViewState | undefined, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @IConfigurationService protected configurationService: IConfigurationService, @ISCMViewService private scmViewService: ISCMViewService, + @IStorageService private storageService: IStorageService, @IUriIdentityService private uriIdentityService: IUriIdentityService, @IContextKeyService contextKeyService: IContextKeyService ) { + // View mode and sort key + this._mode = this.getViewModelMode(); + this._sortKey = this.getViewModelSortKey(); + + // TreeView state + const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); + if (storageViewState) { + try { + this._treeViewState = JSON.parse(storageViewState); + } catch {/* noop */ } + } + this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); - this.modeContextKey.set(_mode); + this.modeContextKey.set(this._mode); this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); - this.sortKeyContextKey.set(_sortKey); + this.sortKeyContextKey.set(this._sortKey); this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); @@ -1143,6 +1167,12 @@ class ViewModel { (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); + + this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + }); } private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { @@ -1432,6 +1462,45 @@ class ViewModel { } } + private getViewModelMode(): ViewModelMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; + const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + + private getViewModelSortKey(): ViewModelSortKey { + // Tree + if (this._mode === ViewModelMode.Tree) { + return ViewModelSortKey.Path; + } + + // List + let viewSortKey: ViewModelSortKey; + const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); + switch (viewSortKeyString) { + case 'name': + viewSortKey = ViewModelSortKey.Name; + break; + case 'status': + viewSortKey = ViewModelSortKey.Status; + break; + default: + viewSortKey = ViewModelSortKey.Path; + break; + } + + const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; + if (typeof storageSortKey === 'string') { + viewSortKey = storageSortKey; + } + + return viewSortKey; + } + dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -1503,6 +1572,56 @@ registerAction2(SetTreeViewModeAction); registerAction2(SetListViewModeNavigationAction); registerAction2(SetTreeViewModeNavigationAction); +abstract class RepositorySortAction extends ViewAction { + constructor(private sortKey: ISCMRepositorySortKey, title: string) { + super({ + id: `workbench.scm.action.repositories.setSortKey.${sortKey}`, + title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: RepositoryContextKeys.RepositorySortKey.isEqualTo(sortKey), + menu: [ + { + id: Menus.Repositories, + group: '1_sort' + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', REPOSITORIES_VIEW_PANE_ID), + group: '1_sort', + }, + ] + }); + } + + runInView(accessor: ServicesAccessor) { + accessor.get(ISCMViewService).toggleSortKey(this.sortKey); + } +} + + +class RepositorySortByDiscoveryTimeAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.DiscoveryTime, localize('repositorySortByDiscoveryTime', "Sort by Discovery Time")); + } +} + +class RepositorySortByNameAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Name, localize('repositorySortByName', "Sort by Name")); + } +} + +class RepositorySortByPathAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Path, localize('repositorySortByPath', "Sort by Path")); + } +} + +registerAction2(RepositorySortByDiscoveryTimeAction); +registerAction2(RepositorySortByNameAction); +registerAction2(RepositorySortByPathAction); + abstract class SetSortKeyAction extends ViewAction { constructor(private sortKey: ViewModelSortKey, title: string) { super({ @@ -1511,6 +1630,7 @@ abstract class SetSortKeyAction extends ViewAction { viewId: VIEW_PANE_ID, f1: false, toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), + precondition: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), menu: { id: Menus.ViewSort, group: '2_sort' } }); } @@ -2058,7 +2178,6 @@ export class SCMViewPane extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, - @IStorageService private storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { @@ -2145,44 +2264,9 @@ export class SCMViewPane extends ViewPane { append(this.listContainer, overflowWidgetsDomNode); - let viewMode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - - const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof storageMode === 'string') { - viewMode = storageMode; - } - - let viewSortKey: ViewModelSortKey; - const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); - switch (viewSortKeyString) { - case 'name': - viewSortKey = ViewModelSortKey.Name; - break; - case 'status': - viewSortKey = ViewModelSortKey.Status; - break; - default: - viewSortKey = ViewModelSortKey.Path; - break; - } - - const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; - if (typeof storageSortKey === 'string') { - viewSortKey = storageSortKey; - } - - let viewState: ITreeViewState | undefined; - - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); - if (storageViewState) { - try { - viewState = JSON.parse(storageViewState); - } catch {/* noop */ } - } - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer, viewMode, viewSortKey, viewState); + this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer); this._register(this._viewModel); this.listContainer.classList.add('file-icon-themable-tree'); @@ -2191,18 +2275,11 @@ export class SCMViewPane extends ViewPane { this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this._register(this._viewModel.onDidChangeSortKey(this.onDidChangeSortKey, this)); this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'))(this.updateActions, this)); this.updateActions(); - - this._register(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this._viewModel.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - })); } private updateIndentStyles(theme: IFileIconTheme): void { @@ -2214,11 +2291,6 @@ export class SCMViewPane extends ViewPane { private onDidChangeMode(): void { this.updateIndentStyles(this.themeService.getFileIconTheme()); - this.storageService.store(`scm.viewMode`, this._viewModel.mode, StorageScope.WORKSPACE, StorageTarget.USER); - } - - private onDidChangeSortKey(): void { - this.storageService.store(`scm.viewSortKey`, this._viewModel.sortKey, StorageScope.WORKSPACE, StorageTarget.USER); } override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2381,6 +2453,10 @@ export class SCMViewPane extends ViewPane { override shouldShowWelcome(): boolean { return this.scmService.repositories.length === 0; } + + override getActionsContext(): unknown { + return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; + } } export const scmProviderSeparatorBorderColor = registerColor('scm.providerBorder', { dark: '#454545', light: '#C8C8C8', hcDark: contrastBorder, hcLight: contrastBorder }, localize('scm.providerBorder', "SCM Provider separator border.")); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 757c5526c7a..be0176c4919 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -5,7 +5,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; -import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMViewService, ISCMRepository, ISCMService, ISCMViewVisibleRepositoryChangeEvent, ISCMMenus, ISCMProvider, ISCMRepositorySortKey } from 'vs/workbench/contrib/scm/common/scm'; import { Iterable } from 'vs/base/common/iterator'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { SCMMenus } from 'vs/workbench/contrib/scm/browser/menus'; @@ -15,6 +15,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { basename } from 'vs/base/common/resources'; import { binarySearch } from 'vs/base/common/arrays'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; function getProviderStorageKey(provider: ISCMProvider): string { return `${provider.contextValue}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`; @@ -29,8 +31,20 @@ function getRepositoryName(workspaceContextService: IWorkspaceContextService, re return folder?.uri.toString() === repository.provider.rootUri.toString() ? folder.name : basename(repository.provider.rootUri); } +export const RepositoryContextKeys = { + RepositorySortKey: new RawContextKey('scmRepositorySortKey', ISCMRepositorySortKey.DiscoveryTime), +}; + +interface ISCMRepositoryView { + readonly repository: ISCMRepository; + readonly discoveryTime: number; + focused: boolean; + selectionIndex: number; +} + export interface ISCMViewServiceState { readonly all: string[]; + readonly sortKey: ISCMRepositorySortKey; readonly visible: number[]; } @@ -45,20 +59,24 @@ export class SCMViewService implements ISCMViewService { private previousState: ISCMViewServiceState | undefined; private disposables = new DisposableStore(); - private _repositories: ISCMRepository[] = []; + private _repositories: ISCMRepositoryView[] = []; get repositories(): ISCMRepository[] { - return this._repositories; + return this._repositories.map(r => r.repository); } - private _onDidChangeRepositories = new Emitter(); - readonly onDidChangeRepositories = this._onDidChangeRepositories.event; - - private _visibleRepositoriesSet = new Set(); - private _visibleRepositories: ISCMRepository[] = []; - get visibleRepositories(): ISCMRepository[] { - return this._visibleRepositories; + // In order to match the legacy behaviour, when the repositories are sorted by discovery time, + // the visible repositories are sorted by the selection index instead of the discovery time. + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return this._repositories.filter(r => r.selectionIndex !== -1) + .sort((r1, r2) => r1.selectionIndex - r2.selectionIndex) + .map(r => r.repository); + } + + return this._repositories + .filter(r => r.selectionIndex !== -1) + .map(r => r.repository); } set visibleRepositories(visibleRepositories: ISCMRepository[]) { @@ -66,15 +84,18 @@ export class SCMViewService implements ISCMViewService { const added = new Set(); const removed = new Set(); - for (const repository of visibleRepositories) { - if (!this._visibleRepositoriesSet.has(repository)) { - added.add(repository); + for (const repositoryView of this._repositories) { + // Selected -> !Selected + if (!set.has(repositoryView.repository) && repositoryView.selectionIndex !== -1) { + repositoryView.selectionIndex = -1; + removed.add(repositoryView.repository); } - } - - for (const repository of this._visibleRepositories) { - if (!set.has(repository)) { - removed.add(repository); + // Selected | !Selected -> Selected + if (set.has(repositoryView.repository)) { + if (repositoryView.selectionIndex === -1) { + added.add(repositoryView.repository); + } + repositoryView.selectionIndex = visibleRepositories.indexOf(repositoryView.repository); } } @@ -82,15 +103,17 @@ export class SCMViewService implements ISCMViewService { return; } - this._visibleRepositories = visibleRepositories.sort(this._compareRepositories); - this._visibleRepositoriesSet = set; this._onDidSetVisibleRepositories.fire({ added, removed }); - if (this._focusedRepository && removed.has(this._focusedRepository)) { - this.focus(this._visibleRepositories[0]); + // Update focus if the focused repository is not visible anymore + if (this._repositories.find(r => r.focused && r.selectionIndex === -1)) { + this.focus(this._repositories.find(r => r.selectionIndex !== -1)?.repository); } } + private _onDidChangeRepositories = new Emitter(); + readonly onDidChangeRepositories = this._onDidChangeRepositories.event; + private _onDidSetVisibleRepositories = new Emitter(); readonly onDidChangeVisibleRepositories = Event.any( this._onDidSetVisibleRepositories.event, @@ -101,53 +124,61 @@ export class SCMViewService implements ISCMViewService { return e; } - return { - added: Iterable.concat(last.added, e.added), - removed: Iterable.concat(last.removed, e.removed), - }; + const added = new Set(last.added); + const removed = new Set(last.removed); + + for (const repository of e.added) { + if (removed.has(repository)) { + removed.delete(repository); + } else { + added.add(repository); + } + } + for (const repository of e.removed) { + if (added.has(repository)) { + added.delete(repository); + } else { + removed.add(repository); + } + } + + return { added, removed }; }, 0) ); - private _focusedRepository: ISCMRepository | undefined; - get focusedRepository(): ISCMRepository | undefined { - return this._focusedRepository; + return this._repositories.find(r => r.focused)?.repository; } private _onDidFocusRepository = new Emitter(); readonly onDidFocusRepository = this._onDidFocusRepository.event; - private _compareRepositories: (op1: ISCMRepository, op2: ISCMRepository) => number; + private _repositoriesSortKey: ISCMRepositorySortKey; + private _sortKeyContextKey: IContextKey; constructor( - @ISCMService private readonly scmService: ISCMService, + @ISCMService scmService: ISCMService, + @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, - @IWorkspaceContextService workspaceContextService: IWorkspaceContextService + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService ) { this.menus = instantiationService.createInstance(SCMMenus); - this._compareRepositories = (op1: ISCMRepository, op2: ISCMRepository): number => { - const name1 = getRepositoryName(workspaceContextService, op1); - const name2 = getRepositoryName(workspaceContextService, op2); - - const nameComparison = compareFileNames(name1, name2); - if (nameComparison === 0 && op1.provider.rootUri && op2.provider.rootUri) { - return comparePaths(op1.provider.rootUri.fsPath, op2.provider.rootUri.fsPath); - } - - return nameComparison; - }; - - scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); - scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); - try { this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, '')); } catch { // noop } + this._repositoriesSortKey = this.previousState?.sortKey ?? this.getViewSortOrder(); + this._sortKeyContextKey = RepositoryContextKeys.RepositorySortKey.bindTo(contextKeyService); + this._sortKeyContextKey.set(this._repositoriesSortKey); + + scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); + scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables); + for (const repository of scmService.repositories) { this.onDidAddRepository(repository); } @@ -160,50 +191,61 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } - this.insertRepository(this._repositories, repository); + const repositoryView: ISCMRepositoryView = { + repository, discoveryTime: Date.now(), focused: false, selectionIndex: -1 + }; + let removed: Iterable = Iterable.empty(); if (this.previousState) { const index = this.previousState.all.indexOf(getProviderStorageKey(repository.provider)); - if (index === -1) { // saw a repo we did not expect + if (index === -1) { + // This repository is not part of the previous state which means that it + // was either manually closed in the previous session, or the repository + // was added after the previous session.In this case, we should select all + // of the repositories. const added: ISCMRepository[] = []; - for (const repo of this.scmService.repositories) { // all should be visible - if (!this._visibleRepositoriesSet.has(repo)) { - added.push(repository); - } - } - this._visibleRepositoriesSet = new Set(this.scmService.repositories); - this._visibleRepositories = [...this.scmService.repositories.sort(this._compareRepositories)]; + this.insertRepositoryView(this._repositories, repositoryView); + this._repositories.forEach((repositoryView, index) => { + if (repositoryView.selectionIndex === -1) { + added.push(repositoryView.repository); + } + repositoryView.selectionIndex = index; + }); + this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() }); - this.finishLoading(); + this.didSelectRepository = false; return; } - if (this.previousState.visible.indexOf(index) > -1) { - // First visible repository - if (!this.didSelectRepository) { - removed = this._visibleRepositories; - - this._visibleRepositories = []; - this._visibleRepositoriesSet = new Set(); - this.didSelectRepository = true; - } - } else { + if (this.previousState.visible.indexOf(index) === -1) { // Explicit selection started if (this.didSelectRepository) { + this.insertRepositoryView(this._repositories, repositoryView); this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); return; } + } else { + // First visible repository + if (!this.didSelectRepository) { + removed = [...this.visibleRepositories]; + this._repositories.forEach(r => { + r.focused = false; + r.selectionIndex = -1; + }); + + this.didSelectRepository = true; + } } } - this._visibleRepositoriesSet.add(repository); - this.insertRepository(this._visibleRepositories, repository); - this._onDidChangeRepositories.fire({ added: [repository], removed }); + const maxSelectionIndex = this.getMaxSelectionIndex(); + this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 }); + this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed }); - if (!this._focusedRepository) { + if (!this._repositories.find(r => r.focused)) { this.focus(repository); } } @@ -213,39 +255,29 @@ export class SCMViewService implements ISCMViewService { this.eventuallyFinishLoading(); } + const repositoriesIndex = this._repositories.findIndex(r => r.repository === repository); + + if (repositoriesIndex === -1) { + return; + } + let added: Iterable = Iterable.empty(); + const repositoryView = this._repositories.splice(repositoriesIndex, 1); - const repositoriesIndex = this._repositories.indexOf(repository); - const visibleRepositoriesIndex = this._visibleRepositories.indexOf(repository); - - if (repositoriesIndex > -1) { - this._repositories.splice(repositoriesIndex, 1); + if (this._repositories.length > 0 && this.visibleRepositories.length === 0) { + this._repositories[0].selectionIndex = 0; + added = [this._repositories[0].repository]; } - if (visibleRepositoriesIndex > -1) { - this._visibleRepositories.splice(visibleRepositoriesIndex, 1); - this._visibleRepositoriesSet.delete(repository); + this._onDidChangeRepositories.fire({ added, removed: repositoryView.map(r => r.repository) }); - if (this._repositories.length > 0 && this._visibleRepositories.length === 0) { - const first = this._repositories[0]; - - this._visibleRepositories.push(first); - this._visibleRepositoriesSet.add(first); - added = [first]; - } - } - - if (repositoriesIndex > -1 || visibleRepositoriesIndex > -1) { - this._onDidChangeRepositories.fire({ added, removed: [repository] }); - } - - if (this._focusedRepository === repository) { - this.focus(this._visibleRepositories[0]); + if (repositoryView.length === 1 && repositoryView[0].focused && this.visibleRepositories.length > 0) { + this.focus(this.visibleRepositories[0]); } } isVisible(repository: ISCMRepository): boolean { - return this._visibleRepositoriesSet.has(repository); + return this._repositories.find(r => r.repository === repository)?.selectionIndex !== -1; } toggleVisibility(repository: ISCMRepository, visible?: boolean): void { @@ -269,18 +301,71 @@ export class SCMViewService implements ISCMViewService { } } + toggleSortKey(sortKey: ISCMRepositorySortKey): void { + this._repositoriesSortKey = sortKey; + this._sortKeyContextKey.set(this._repositoriesSortKey); + this._repositories.sort(this.compareRepositories.bind(this)); + + this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() }); + } + focus(repository: ISCMRepository | undefined): void { - if (repository && !this.visibleRepositories.includes(repository)) { + if (repository && !this.isVisible(repository)) { return; } - this._focusedRepository = repository; - this._onDidFocusRepository.fire(repository); + this._repositories.forEach(r => r.focused = r.repository === repository); + + if (this._repositories.find(r => r.focused)) { + this._onDidFocusRepository.fire(repository); + } } - private insertRepository(repositories: ISCMRepository[], repository: ISCMRepository): void { - const index = binarySearch(repositories, repository, this._compareRepositories); - repositories.splice(index < 0 ? ~index : index, 0, repository); + private compareRepositories(op1: ISCMRepositoryView, op2: ISCMRepositoryView): number { + // Sort by discovery time + if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { + return op1.discoveryTime - op2.discoveryTime; + } + + // Sort by path + if (this._repositoriesSortKey === 'path' && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + // Sort by name, path + const name1 = getRepositoryName(this.workspaceContextService, op1.repository); + const name2 = getRepositoryName(this.workspaceContextService, op2.repository); + + const nameComparison = compareFileNames(name1, name2); + if (nameComparison === 0 && op1.repository.provider.rootUri && op2.repository.provider.rootUri) { + return comparePaths(op1.repository.provider.rootUri.fsPath, op2.repository.provider.rootUri.fsPath); + } + + return nameComparison; + } + + private getMaxSelectionIndex(): number { + return this._repositories.length === 0 ? -1 : + Math.max(...this._repositories.map(r => r.selectionIndex)); + } + + private getViewSortOrder(): ISCMRepositorySortKey { + const sortOder = this.configurationService.getValue<'discovery time' | 'name' | 'path'>('scm.repositories.sortOrder'); + switch (sortOder) { + case 'discovery time': + return ISCMRepositorySortKey.DiscoveryTime; + case 'name': + return ISCMRepositorySortKey.Name; + case 'path': + return ISCMRepositorySortKey.Path; + default: + return ISCMRepositorySortKey.DiscoveryTime; + } + } + + private insertRepositoryView(repositories: ISCMRepositoryView[], repositoryView: ISCMRepositoryView): void { + const index = binarySearch(repositories, repositoryView, this.compareRepositories.bind(this)); + repositories.splice(index < 0 ? ~index : index, 0, repositoryView); } private onWillSaveState(): void { @@ -290,7 +375,7 @@ export class SCMViewService implements ISCMViewService { const all = this.repositories.map(r => getProviderStorageKey(r.provider)); const visible = this.visibleRepositories.map(r => all.indexOf(getProviderStorageKey(r.provider))); - const raw = JSON.stringify({ all, visible }); + const raw = JSON.stringify({ all, sortKey: this._repositoriesSortKey, visible }); this.storageService.store('scm:view:visibleRepositories', raw, StorageScope.WORKSPACE, StorageTarget.MACHINE); } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index c46af5c54b5..a70726447a1 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -168,6 +168,12 @@ export interface ISCMMenus { getRepositoryMenus(provider: ISCMProvider): ISCMRepositoryMenus; } +export const enum ISCMRepositorySortKey { + DiscoveryTime = 'discoveryTime', + Name = 'name', + Path = 'path' +} + export const ISCMViewService = createDecorator('scmView'); export interface ISCMViewVisibleRepositoryChangeEvent { @@ -189,6 +195,8 @@ export interface ISCMViewService { isVisible(repository: ISCMRepository): boolean; toggleVisibility(repository: ISCMRepository, visible?: boolean): void; + toggleSortKey(sortKey: ISCMRepositorySortKey): void; + readonly focusedRepository: ISCMRepository | undefined; readonly onDidFocusRepository: Event; focus(repository: ISCMRepository): void; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 7cec0e4491a..a587b74597b 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -8,7 +8,6 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree'; import { IAction } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; @@ -1065,10 +1064,10 @@ export class SearchView extends ViewPane { this.inputPatternExcludes.setWidth(this.size.width - 28 /* container margin */); this.inputPatternIncludes.setWidth(this.size.width - 28 /* container margin */); - const widgetHeight = dom.getTotalHeight(this.searchWidget.domNode); + + const widgetHeight = dom.getTotalHeight(this.searchWidgetsContainerElement); const messagesHeight = dom.getTotalHeight(this.messagesElement); - const margin = 25; - this.tree.layout(this.size.height - widgetHeight - messagesHeight - margin, this.size.width - 28); + this.tree.layout(this.size.height - widgetHeight - messagesHeight, this.size.width - 28); } protected override layoutBody(height: number, width: number): void { @@ -1285,7 +1284,7 @@ export class SearchView extends ViewPane { } if (!skipLayout && this.size) { - this.layout(this._orientation === Orientation.VERTICAL ? this.size.height : this.size.width); + this.reLayout(); } } diff --git a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts index d1a74339e79..cfcbec62d50 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts @@ -43,6 +43,7 @@ import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbe import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { LanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { staticObservableValue } from 'vs/base/common/observableValue'; // declare var __dirname: string; @@ -182,7 +183,7 @@ suite.skip('TextSearch performance (integration)', () => { class TestTelemetryService implements ITelemetryService { public _serviceBrand: undefined; - public telemetryLevel = TelemetryLevel.USAGE; + public telemetryLevel = staticObservableValue(TelemetryLevel.USAGE); public sendErrorTelemetry = true; public events: any[] = []; diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts index 395145ee8b4..8e263a87401 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTags.ts @@ -36,7 +36,7 @@ export class WorkspaceTags implements IWorkbenchContribution { @IProductService private readonly productService: IProductService, @INativeHostService private readonly nativeHostService: INativeHostService ) { - if (this.telemetryService.telemetryLevel === TelemetryLevel.USAGE) { + if (this.telemetryService.telemetryLevel.value === TelemetryLevel.USAGE) { this.report(); } } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 70c269e4a69..2e0f5f76e40 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -41,7 +41,7 @@ import { IConfigurationResolverService } from 'vs/workbench/services/configurati import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IOutputService, IOutputChannel } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService, IOutputChannel } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c0547a2ae39..992ecb05aec 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -29,7 +29,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITerminalService, ITerminalInstance, ITerminalGroupService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 436f0194e19..a17d4185861 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -30,7 +30,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh new file mode 100644 index 00000000000..26ab335881a --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ $options[norcs] = off && -o "login" && -f ~/.zshenv ]]; then + . ~/.zshenv +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh new file mode 100644 index 00000000000..734bb831e11 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh @@ -0,0 +1,8 @@ +# --------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# --------------------------------------------------------------------------------------------- + +if [[ $options[norcs] = off && -o "login" && -f ~/.zprofile ]]; then + . ~/.zprofile +fi diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh index cfba2786a3e..2dec005daf4 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.zsh @@ -12,13 +12,8 @@ builtin unset ZDOTDIR # as disable it by unsetting the variable. VSCODE_SHELL_INTEGRATION=1 -if [ -f ~/.zshenv ]; then - . ~/.zshenv -fi -if [[ -o "login" && -f ~/.zprofile ]]; then - . ~/.zprofile -fi -if [ -f ~/.zshrc ]; then + +if [[ $options[norcs] = off && -f ~/.zshrc ]]; then . ~/.zshrc fi diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 6168afff93b..ffd839a6c18 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -10,6 +10,8 @@ import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { TerminalLocation } from 'vs/platform/terminal/common/terminal'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class TerminalFindWidget extends SimpleFindWidget { protected _findInputFocused: IContextKey; @@ -21,7 +23,9 @@ export class TerminalFindWidget extends SimpleFindWidget { @IContextViewService _contextViewService: IContextViewService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITerminalService private readonly _terminalService: ITerminalService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { super(_contextViewService, _contextKeyService, findState, { showOptionButtons: true, showResultCount: true }); @@ -31,15 +35,25 @@ export class TerminalFindWidget extends SimpleFindWidget { this._findInputFocused = TerminalContextKeys.findInputFocus.bindTo(this._contextKeyService); this._findWidgetFocused = TerminalContextKeys.findFocus.bindTo(this._contextKeyService); this._findWidgetVisible = TerminalContextKeys.findVisible.bindTo(_contextKeyService); + this._register(this._themeService.onDidColorThemeChange(() => { + if (this._findWidgetVisible) { + this.find(true, true); + } + })); + this._register(this._configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('workbench.colorCustomizations') && this._findWidgetVisible) { + this.find(true, true); + } + })); } - find(previous: boolean) { + find(previous: boolean, update?: boolean) { const instance = this._terminalService.activeInstance; if (!instance) { return; } if (previous) { - instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); + instance.xterm?.findPrevious(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue(), incremental: update }); } else { instance.xterm?.findNext(this.inputValue, { regex: this._getRegexValue(), wholeWord: this._getWholeWordValue(), caseSensitive: this._getCaseSensitiveValue() }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 18be1c0ae4a..0adfa2cee9d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -460,6 +460,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Re-establish the title after reconnect if (this.shellLaunchConfig.attachPersistentProcess) { this.refreshTabLabels(this.shellLaunchConfig.attachPersistentProcess.title, this.shellLaunchConfig.attachPersistentProcess.titleSource); + this.setShellType(this.shellType); } if (this._fixedCols) { @@ -1098,11 +1099,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(dom.addDisposableListener(xterm.raw.textarea, 'focus', () => { this._terminalFocusContextKey.set(true); - if (this.shellType) { - this._terminalShellTypeContextKey.set(this.shellType.toString()); - } else { - this._terminalShellTypeContextKey.reset(); - } this._onDidFocus.fire(this); })); @@ -1882,6 +1878,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { setShellType(shellType: TerminalShellType) { this._shellType = shellType; + if (shellType) { + this._terminalShellTypeContextKey.set(shellType?.toString()); + } } private _setAriaLabel(xterm: XTermTerminal | undefined, terminalId: number, title: string | undefined): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 8402e0e5362..62d52e82cc9 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -52,6 +52,7 @@ export class TerminalService implements ITerminalService { private _hostActiveTerminals: Map = new Map(); private _terminalEditorActive: IContextKey; + private readonly _terminalShellTypeContextKey: IContextKey; private _escapeSequenceLoggingEnabled: boolean = false; @@ -187,9 +188,15 @@ export class TerminalService implements ITerminalService { if (!instance && !this._isShuttingDown) { this._terminalGroupService.hidePanel(); } + if (instance?.shellType) { + this._terminalShellTypeContextKey.set(instance.shellType.toString()); + } else if (!instance) { + this._terminalShellTypeContextKey.reset(); + } }); this._handleInstanceContextKeys(); + this._terminalShellTypeContextKey = TerminalContextKeys.shellType.bindTo(this._contextKeyService); this._processSupportContextKey = TerminalContextKeys.processSupported.bindTo(this._contextKeyService); this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); this._terminalHasBeenCreated = TerminalContextKeys.terminalHasBeenCreated.bindTo(this._contextKeyService); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index 40b75ff1352..3f07b624fa7 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -73,6 +73,8 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { this._refreshStyles(); } else if (e.affectsConfiguration(TerminalSettingId.FontSize) || e.affectsConfiguration(TerminalSettingId.LineHeight)) { this.refreshLayouts(); + } else if (e.affectsConfiguration('workbench.colorCustomizations')) { + this._refreshStyles(true); } }); this._themeService.onDidColorThemeChange(() => this._refreshStyles(true)); @@ -94,10 +96,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon { } else { color = ''; } - if (decoration.decoration.overviewRulerOptions) { - decoration.decoration.overviewRulerOptions.color = color; - } else { - decoration.decoration.overviewRulerOptions = { color }; + if (decoration.decoration.options?.overviewRulerOptions) { + decoration.decoration.options.overviewRulerOptions.color = color; + } else if (decoration.decoration.options) { + decoration.decoration.options.overviewRulerOptions = { color }; } } } diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 4020b3e6fa0..3ddae516bb0 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -186,7 +186,7 @@ const terminalConfiguration: IConfigurationNode = { default: DEFAULT_LINE_HEIGHT }, [TerminalSettingId.MinimumContrastRatio]: { - markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set the foreground color of each cell will change to try meet the contrast ratio specified. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), + markdownDescription: localize('terminal.integrated.minimumContrastRatio', "When set, the foreground color of each cell will change to try meet the contrast ratio specified. Note that this will not apply to `powerline` characters per #146406. Example values:\n\n- 1: Do nothing and use the standard theme colors.\n- 4.5: [WCAG AA compliance (minimum)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html) (default).\n- 7: [WCAG AAA compliance (enhanced)](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast7.html).\n- 21: White on black or black on white."), type: 'number', default: 4.5 }, diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts index c32661787e3..4d1af363aa7 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerFilter.ts @@ -65,6 +65,11 @@ export class TestingExplorerFilter extends BaseActionViewItem { const wrapper = this.wrapper = dom.$('.testing-filter-wrapper'); container.appendChild(wrapper); + const history = this.history.get([]); + if (history.length) { + this.state.setText(history[history.length - 1]); + } + const input = this.input = this._register(this.instantiationService.createInstance(ContextScopedSuggestEnabledInputWithHistory, { id: 'testing.explorer.filter', ariaLabel: localize('testExplorerFilterLabel', "Filter text for tests in the explorer"), @@ -89,7 +94,7 @@ export class TestingExplorerFilter extends BaseActionViewItem { value: this.state.text.value, placeholderText: localize('testExplorerFilter', "Filter (e.g. text, !exclude, @tag)"), }, - history: this.history.get([]) + history })); this._register(attachSuggestEnabledInputBoxStyler(input, this.themeService)); diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 10c04d75a9c..6c2233986e1 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -571,9 +571,10 @@ export class TimelinePane extends ViewPane { } } request?.tokenSource.dispose(true); - + options.cacheResults = true; + options.resetCache = reset; request = this.timelineService.getTimeline( - source, uri, options, new CancellationTokenSource(), { cacheResults: true, resetCache: reset } + source, uri, options, new CancellationTokenSource() ); if (request === undefined) { diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index 8b9f870c14d..5185f65703e 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -77,11 +77,8 @@ export interface TimelineChangeEvent { export interface TimelineOptions { cursor?: string; limit?: number | { timestamp: number; id?: string }; -} - -export interface InternalTimelineOptions { - cacheResults: boolean; - resetCache: boolean; + resetCache?: boolean; + cacheResults?: boolean; } export interface Timeline { @@ -101,7 +98,7 @@ export interface Timeline { export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { onDidChange?: Event; - provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise; + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken): Promise; } export interface TimelineSource { @@ -152,7 +149,7 @@ export interface ITimelineService { getSources(): TimelineSource[]; - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource): TimelineRequest | undefined; setUri(uri: URI): void; } diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 52bc586f90d..190dfa2501c 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions, TimelinePaneId } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, TimelinePaneId } from './timeline'; import { IViewsService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -44,7 +44,7 @@ export class TimelineService implements ITimelineService { return [...this.providers.values()].map(p => ({ id: p.id, label: p.label })); } - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString()}`); const provider = this.providers.get(id); @@ -61,7 +61,7 @@ export class TimelineService implements ITimelineService { } return { - result: provider.provideTimeline(uri, options, tokenSource.token, internalOptions) + result: provider.provideTimeline(uri, options, tokenSource.token) .then(result => { if (result === undefined) { return undefined; diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 07d50613857..4a7fe8b6202 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -37,7 +37,7 @@ import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/ed import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; -import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { IOutputService } from 'vs/workbench/services/output/common/output'; import { IActivityService, IBadge, NumberBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 48beccb020a..d2c57f4aa10 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -368,16 +368,14 @@ const unloadMonitor = new class { } switch (this.confirmBeforeClose) { - case 'always': - { - event.preventDefault(); - event.returnValue = ''; - return ''; - } - case 'never': - { - break; - } + case 'always': { + event.preventDefault(); + event.returnValue = ''; + return ''; + } + case 'never': { + break; + } case 'keyboardOnly': default: { if (this.isModifierKeyDown) { @@ -680,6 +678,22 @@ const handleInnerScroll = (event) => { }); }; +function handleInnerDragStartEvent(/** @type {DragEvent} */ e) { + if (e.defaultPrevented) { + // Extension code has already handled this event + return; + } + + if (!e.dataTransfer || e.shiftKey) { + return; + } + + // Only handle drags from outside editor for now + if (e.dataTransfer.items.length && Array.prototype.every.call(e.dataTransfer.items, item => item.kind === 'file')) { + hostMessaging.postMessage('drag-start'); + } +} + /** * @param {() => void} callback */ @@ -1021,6 +1035,9 @@ onDomReady(() => { }); }); + contentWindow.addEventListener('dragenter', handleInnerDragStartEvent); + contentWindow.addEventListener('dragover', handleInnerDragStartEvent); + unloadMonitor.onIframeLoaded(newFrame); } }); @@ -1107,5 +1124,10 @@ onDomReady(() => { } }; + // Also forward events before the contents of the webview have loaded + window.addEventListener('keydown', handleInnerKeydown); + window.addEventListener('dragenter', handleInnerDragStartEvent); + window.addEventListener('dragover', handleInnerDragStartEvent); + hostMessaging.signalReady(); }); diff --git a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js index 70534ee470d..81a45ab6ca7 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/service-worker.js +++ b/src/vs/workbench/contrib/webview/browser/pre/service-worker.js @@ -189,8 +189,10 @@ sw.addEventListener('fetch', (event) => { } // If we're making a request against the remote authority, we want to go - // back through VS Code itself so that we are authenticated properly - if (requestUrl.host === remoteAuthority) { + // through VS Code itself so that we are authenticated properly. If the + // service worker is hosted on the same origin we will have cookies and + // authentication will not be an issue. + if (requestUrl.origin !== sw.origin && requestUrl.host === remoteAuthority) { switch (event.request.method) { case 'GET': case 'HEAD': diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index c790753eb0b..6cd87f63f28 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isFirefox } from 'vs/base/browser/browser'; -import { addDisposableListener } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IAction } from 'vs/base/common/actions'; import { ThrottledDelayer } from 'vs/base/common/async'; @@ -59,6 +59,7 @@ export const enum WebviewMessageChannels { didKeydown = 'did-keydown', didKeyup = 'did-keyup', didContextMenu = 'did-context-menu', + dragStart = 'drag-start', } interface IKeydownEvent { @@ -350,6 +351,10 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } })); + this._register(this.on(WebviewMessageChannels.dragStart, () => { + this.startBlockingIframeDragEvents(); + })); + if (options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); this.styledFindWidget(); @@ -489,9 +494,32 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (this._webviewFindWidget) { parent.appendChild(this._webviewFindWidget.getDomNode()); } + + [EventType.MOUSE_DOWN, EventType.MOUSE_MOVE, EventType.DROP].forEach(eventName => { + this._register(addDisposableListener(parent, eventName, () => { + this.stopBlockingIframeDragEvents(); + })); + }); + + [parent, window].forEach(node => this._register(addDisposableListener(node as HTMLElement, EventType.DRAG_END, () => { + this.stopBlockingIframeDragEvents(); + }))); + parent.appendChild(this.element); } + private startBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'none'; + } + } + + private stopBlockingIframeDragEvents() { + if (this.element) { + this.element.style.pointerEvents = 'auto'; + } + } + protected webviewContentEndpoint(encodedWebviewOrigin: string): string { const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin); if (endpoint[endpoint.length - 1] === '/') { @@ -685,18 +713,14 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } windowDidDragStart(): void { - // Webview break drag and droping around the main window (no events are generated when you are over them) + // Webview break drag and dropping around the main window (no events are generated when you are over them) // Work around this by disabling pointer events during the drag. // https://github.com/electron/electron/issues/18226 - if (this.element) { - this.element.style.pointerEvents = 'none'; - } + this.startBlockingIframeDragEvents(); } windowDidDragEnd(): void { - if (this.element) { - this.element.style.pointerEvents = ''; - } + this.stopBlockingIframeDragEvents(); } public selectAll() { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts index 1caa0e68f25..63e3f1cc124 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInput.ts @@ -23,7 +23,7 @@ export class WebviewInput extends EditorInput { } public override get capabilities(): EditorInputCapabilities { - return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton; + return EditorInputCapabilities.Readonly | EditorInputCapabilities.Singleton | EditorInputCapabilities.CanDropIntoEditor; } private _name: string; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts index 75b3891db8d..4be26d8aedc 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts @@ -340,7 +340,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ } if (step.media.image) { - const altText = (step.media as any).altText; + const altText = step.media.altText; if (altText === undefined) { console.error('Walkthrough item:', fullyQualifiedID, 'is missing altText for its media element.'); } @@ -362,7 +362,7 @@ export class WalkthroughsService extends Disposable implements IWalkthroughsServ }; } - // Legacy media config + // Legacy media config (only in use by remote-wsl at the moment) else { const legacyMedia = step.media as unknown as { path: string; altText: string }; if (typeof legacyMedia.path === 'string' && legacyMedia.path.endsWith('.md')) { diff --git a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts index 7e54169a228..8b87c52e229 100644 --- a/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts +++ b/src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts @@ -40,8 +40,8 @@ registerAction2(class extends Action2 { }); } - run(accessor: ServicesAccessor) { - assertIsDefined(NewFileTemplatesManager.Instance).run(); + async run(accessor: ServicesAccessor): Promise { + return assertIsDefined(NewFileTemplatesManager.Instance).run(); } }); @@ -79,20 +79,26 @@ class NewFileTemplatesManager extends Disposable { return items; } - run() { + async run(): Promise { const entries = this.allEntries(); if (entries.length === 0) { throw Error('Unexpected empty new items list'); } else if (entries.length === 1) { this.commandService.executeCommand(entries[0].commandID); + return true; } else { - this.selectNewEntry(entries); + return this.selectNewEntry(entries); } } - private async selectNewEntry(entries: NewFileItem[]) { + private async selectNewEntry(entries: NewFileItem[]): Promise { + let resolveResult: (res: boolean) => void; + const resultPromise = new Promise(resolve => { + resolveResult = resolve; + }); + const disposables = new DisposableStore(); const qp = this.quickInputService.createQuickPick(); qp.title = localize('createNew', "Create New..."); @@ -158,6 +164,8 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidAccept(async e => { const selected = qp.selectedItems[0] as (IQuickPickItem & NewFileItem); + resolveResult(!!selected); + qp.hide(); if (selected) { await this.commandService.executeCommand(selected.commandID); } })); @@ -165,14 +173,18 @@ class NewFileTemplatesManager extends Disposable { disposables.add(qp.onDidHide(() => { qp.dispose(); disposables.dispose(); + resolveResult(false); })); disposables.add(qp.onDidTriggerItemButton(e => { qp.hide(); this.commandService.executeCommand('workbench.action.openGlobalKeybindings', (e.item as (IQuickPickItem & NewFileItem)).commandID); + resolveResult(false); })); qp.show(); + + return resultPromise; } } diff --git a/src/vs/workbench/electron-sandbox/actions/windowActions.ts b/src/vs/workbench/electron-sandbox/actions/windowActions.ts index 8ab7286bcfe..a89681fa292 100644 --- a/src/vs/workbench/electron-sandbox/actions/windowActions.ts +++ b/src/vs/workbench/electron-sandbox/actions/windowActions.ts @@ -227,6 +227,7 @@ abstract class BaseSwitchWindow extends Action2 { activeItem: picks[autoFocusIndex], placeHolder, quickNavigate: this.isQuickNavigate() ? { keybindings: keybindingService.lookupKeybindings(this.desc.id) } : undefined, + hideInput: this.isQuickNavigate(), onDidTriggerItemButton: async context => { await nativeHostService.closeWindowById(context.item.payload); context.removeItem(); diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 805496f0f2b..8c88a219e24 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -84,21 +84,21 @@ import { ModifierKeyEmitter } from 'vs/base/browser/dom'; // Actions: macOS Native Tabs if (isMacintosh) { - [ + for (const command of [ { handler: NewWindowTabHandler, id: 'workbench.action.newWindowTab', title: { value: localize('newTab', "New Window Tab"), original: 'New Window Tab' } }, { handler: ShowPreviousWindowTabHandler, id: 'workbench.action.showPreviousWindowTab', title: { value: localize('showPreviousTab', "Show Previous Window Tab"), original: 'Show Previous Window Tab' } }, { handler: ShowNextWindowTabHandler, id: 'workbench.action.showNextWindowTab', title: { value: localize('showNextWindowTab', "Show Next Window Tab"), original: 'Show Next Window Tab' } }, { handler: MoveWindowTabToNewWindowHandler, id: 'workbench.action.moveWindowTabToNewWindow', title: { value: localize('moveWindowTabToNewWindow', "Move Window Tab to New Window"), original: 'Move Window Tab to New Window' } }, { handler: MergeWindowTabsHandlerHandler, id: 'workbench.action.mergeAllWindowTabs', title: { value: localize('mergeAllWindowTabs', "Merge All Windows"), original: 'Merge All Windows' } }, { handler: ToggleWindowTabsBarHandler, id: 'workbench.action.toggleWindowTabsBar', title: { value: localize('toggleWindowTabsBar', "Toggle Window Tabs Bar"), original: 'Toggle Window Tabs Bar' } } - ].forEach(command => { + ]) { CommandsRegistry.registerCommand(command.id, command.handler); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: ContextKeyExpr.equals('config.window.nativeTabs', true) }); - }); + } } // Actions: Developer diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts index 6a78e65d27c..7e658a10cdc 100644 --- a/src/vs/workbench/electron-sandbox/desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import product from 'vs/platform/product/common/product'; import { INativeWindowConfiguration, zoomLevelToZoomFactor } from 'vs/platform/window/common/window'; import { Workbench } from 'vs/workbench/browser/workbench'; @@ -83,15 +84,15 @@ export class DesktopMain extends Disposable { // Files const filesToWait = this.configuration.filesToWait; const filesToWaitPaths = filesToWait?.paths; - [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff].forEach(paths => { + for (const paths of [filesToWaitPaths, this.configuration.filesToOpenOrCreate, this.configuration.filesToDiff]) { if (Array.isArray(paths)) { - paths.forEach(path => { + for (const path of paths) { if (path.fileUri) { path.fileUri = URI.revive(path.fileUri); } - }); + } } - }); + } if (filesToWait) { filesToWait.waitMarkerFileUri = URI.revive(filesToWait.waitMarkerFileUri); @@ -129,7 +130,7 @@ export class DesktopMain extends Disposable { private registerListeners(workbench: Workbench, storageService: NativeStorageService): void { // Workbench Lifecycle - this._register(workbench.onWillShutdown(event => event.join(storageService.close(), 'join.closeStorage'))); + this._register(workbench.onWillShutdown(event => event.join(storageService.close(), { id: 'join.closeStorage', label: localize('join.closeStorage', "Saving UI state") }))); this._register(workbench.onDidShutdown(() => this.dispose())); } @@ -278,7 +279,7 @@ export class DesktopMain extends Disposable { const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService, logService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, remoteAuthorityResolverService, storageService, uriIdentityService, environmentService, configurationService, workspaceTrustEnablementService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); // Update workspace trust so that configuration is updated accordingly diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index 9e53d9f7590..d31a27b3881 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -63,7 +63,7 @@ import { whenEditorClosed } from 'vs/workbench/browser/editor'; import { ISharedProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { registerLegacyWindowDriver, registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; +import { registerWindowDriver } from 'vs/platform/driver/electron-sandbox/driver'; export class NativeWindow extends Disposable { @@ -132,11 +132,11 @@ export class NativeWindow extends Disposable { this._register(this.editorService.onDidActiveEditorChange(() => this.updateTouchbarMenu())); // prevent opening a real URL inside the window - [EventType.DRAG_OVER, EventType.DROP].forEach(event => { + for (const event of [EventType.DRAG_OVER, EventType.DROP]) { window.document.body.addEventListener(event, (e: DragEvent) => { EventHelper.stop(e); }); - }); + } // Support runAction event ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => { @@ -359,7 +359,7 @@ export class NativeWindow extends Disposable { // Progress for long running shutdown if (confirmed) { - this.progressOnShutdown(reason); + this.progressOnBeforeShutdown(reason); } return !confirmed; @@ -368,10 +368,10 @@ export class NativeWindow extends Disposable { } // Progress for long running shutdown - this.progressOnShutdown(reason); + this.progressOnBeforeShutdown(reason); } - private progressOnShutdown(reason: ShutdownReason): void { + private progressOnBeforeShutdown(reason: ShutdownReason): void { this.progressService.withProgress({ location: ProgressLocation.Window, // use window progress to not be too annoying about this operation delay: 800, // delay so that it only appears when operation takes a long time @@ -419,19 +419,29 @@ export class NativeWindow extends Disposable { }); } - private onWillShutdown({ reason, force }: WillShutdownEvent): void { - this.progressService.withProgress({ - location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now - buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway - delay: 800, // delay so that it only appears when operation takes a long time - cancellable: false, // do not allow to cancel - sticky: true, // do not allow to dismiss - title: this.toShutdownLabel(reason, false) - }, () => { - return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown - }, () => { - force(); - }); + private onWillShutdown({ reason, force, joiners }: WillShutdownEvent): void { + + // Delay so that the dialog only appears after timeout + const shutdownDialogScheduler = new RunOnceScheduler(() => { + const pendingJoiners = joiners(); + + this.progressService.withProgress({ + location: ProgressLocation.Dialog, // use a dialog to prevent the user from making any more interactions now + buttons: [this.toForceShutdownLabel(reason)], // allow to force shutdown anyway + cancellable: false, // do not allow to cancel + sticky: true, // do not allow to dismiss + title: this.toShutdownLabel(reason, false), + detail: pendingJoiners.length > 0 ? localize('willShutdownDetail', "The following operations are still running: \n{0}", pendingJoiners.map(joiner => `- ${joiner.label}`).join('\n')) : undefined + }, () => { + return Event.toPromise(this.lifecycleService.onDidShutdown); // dismiss this dialog when we actually shutdown + }, () => { + force(); + }); + }, 1200); + shutdownDialogScheduler.schedule(); + + // Dispose scheduler when we actually shutdown + Event.once(this.lifecycleService.onDidShutdown)(() => shutdownDialogScheduler.dispose()); } private toShutdownLabel(reason: ShutdownReason, isError: boolean): string { @@ -640,27 +650,18 @@ export class NativeWindow extends Disposable { } // Smoke Test Driver - this.setupDriver(); + if (this.environmentService.enableSmokeTestDriver) { + this.setupDriver(); + } } private setupDriver(): void { - - // Modern Driver - if (this.environmentService.enableSmokeTestDriver) { - const that = this; - registerWindowDriver({ - async exitApplication(): Promise { - that.nativeHostService.quit(); - - return that.environmentService.mainPid; - } - }); - } - - // Legacy Driver (TODO@bpasero remove me eventually) - else if (this.environmentService.args.driver) { - this.instantiationService.invokeFunction(async accessor => this._register(await registerLegacyWindowDriver(accessor, this.nativeHostService.windowId))); - } + const that = this; + registerWindowDriver({ + async exitApplication(): Promise { + return that.nativeHostService.quit(); + } + }); } private setupOpenHandlers(): void { @@ -811,9 +812,9 @@ export class NativeWindow extends Disposable { private doAddFolders(): void { const foldersToAdd: IWorkspaceFolderCreationData[] = []; - this.pendingFoldersToAdd.forEach(folder => { + for (const folder of this.pendingFoldersToAdd) { foldersToAdd.push(({ uri: folder })); - }); + } this.pendingFoldersToAdd = []; diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index eb895e4de5e..1e4bcf88ab9 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -30,6 +30,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOpenSource } from 'vs/platform/editor/common/editor'; +import { ILogService } from 'vs/platform/log/common/log'; export abstract class AbstractFileDialogService implements IFileDialogService { @@ -51,7 +52,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService { @IPathService private readonly pathService: IPathService, @ICommandService protected readonly commandService: ICommandService, @IEditorService protected readonly editorService: IEditorService, - @ICodeEditorService protected readonly codeEditorService: ICodeEditorService + @ICodeEditorService protected readonly codeEditorService: ICodeEditorService, + @ILogService private readonly logService: ILogService ) { } async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): Promise { @@ -113,13 +115,24 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { - if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { - return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev testing mode because we cannot assume we run interactive + if (this.skipDialogs()) { + this.logService.trace('FileDialogService: refused to show save confirmation dialog in tests.'); + + // no veto when we are in extension dev testing mode because we cannot assume we run interactive + return ConfirmResult.DONT_SAVE; } return this.doShowSaveConfirm(fileNamesOrResources); } + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + private async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { if (fileNamesOrResources.length === 0) { return ConfirmResult.DONT_SAVE; diff --git a/src/vs/workbench/services/dialogs/common/dialogService.ts b/src/vs/workbench/services/dialogs/common/dialogService.ts index d17f9fbbf5c..7360fe7d875 100644 --- a/src/vs/workbench/services/dialogs/common/dialogService.ts +++ b/src/vs/workbench/services/dialogs/common/dialogService.ts @@ -8,6 +8,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IInput, IInputResult, IShowResult } from 'vs/platform/dialogs/common/dialogs'; import { DialogsModel } from 'vs/workbench/common/dialogs'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ILogService } from 'vs/platform/log/common/log'; export class DialogService extends Disposable implements IDialogService { @@ -19,25 +21,58 @@ export class DialogService extends Disposable implements IDialogService { readonly onDidShowDialog = this.model.onDidShowDialog; + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @ILogService private readonly logService: ILogService + ) { + super(); + } + + private skipDialogs(): boolean { + if (this.environmentService.isExtensionDevelopment && this.environmentService.extensionTestsLocationURI) { + return true; // integration tests + } + + return !!this.environmentService.enableSmokeTestDriver; // smoke tests + } + async confirm(confirmation: IConfirmation): Promise { + if (this.skipDialogs()) { + this.logService.trace('DialogService: refused to show confirmation dialog in tests.'); + + return { confirmed: true }; + } + const handle = this.model.show({ confirmArgs: { confirmation } }); return await handle.result as IConfirmationResult; } async show(severity: Severity, message: string, buttons?: string[], options?: IDialogOptions): Promise { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show dialog in tests.'); + } + const handle = this.model.show({ showArgs: { severity, message, buttons, options } }); return await handle.result as IShowResult; } async input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show input dialog in tests.'); + } + const handle = this.model.show({ inputArgs: { severity, message, buttons, inputs, options } }); return await handle.result as IInputResult; } async about(): Promise { + if (this.skipDialogs()) { + throw new Error('DialogService: refused to show about dialog in tests.'); + } + const handle = this.model.show({}); await handle.result; } diff --git a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts index e4468a73200..7d49ad0b2c3 100644 --- a/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-sandbox/fileDialogService.ts @@ -25,6 +25,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ILogService } from 'vs/platform/log/common/log'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { @@ -45,10 +46,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, - configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + configurationService, fileService, openerService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions { diff --git a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts index fbe3a518f48..0bea5b5eb37 100644 --- a/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts +++ b/src/vs/workbench/services/dialogs/test/electron-sandbox/fileDialogService.test.ts @@ -33,6 +33,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; class TestFileDialogService extends FileDialogService { constructor( @@ -53,10 +54,11 @@ class TestFileDialogService extends FileDialogService { @IPathService pathService: IPathService, @ICommandService commandService: ICommandService, @IEditorService editorService: IEditorService, - @ICodeEditorService codeEditorService: ICodeEditorService + @ICodeEditorService codeEditorService: ICodeEditorService, + @ILogService logService: ILogService ) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, - openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService); + openerService, nativeHostService, dialogService, languageService, workspacesService, labelService, pathService, commandService, editorService, codeEditorService, logService); } protected override getSimpleFileDialog() { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a5499a1ef74..d41839b87ba 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -17,6 +17,7 @@ import { parseLineAndColumnAware } from 'vs/base/common/extpath'; import { LogLevelToString } from 'vs/platform/log/common/log'; import { isUndefined } from 'vs/base/common/types'; import { refineServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; export const IBrowserWorkbenchEnvironmentService = refineServiceDecorator(IEnvironmentService); @@ -295,7 +296,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi } @memoize - get filesToOpenOrCreate(): IPath[] | undefined { + get filesToOpenOrCreate(): IPath[] | undefined { if (this.payload) { const fileToOpen = this.payload.get('openFile'); if (fileToOpen) { @@ -307,7 +308,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi return [{ fileUri: fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index 42a05f2344a..8ebc2b75496 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -112,8 +112,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten const allExtensions = await this.getExtensions(); const localWebWorkerExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { - autoStart: true, - extensions: localWebWorkerExtensions + allExtensions: allExtensions, + myExtensions: localWebWorkerExtensions.map(extension => extension.identifier) }; } }; @@ -232,8 +232,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, - allExtensions: this._registry.getAllExtensionDescriptions() + allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }; } diff --git a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts index 85bff25d857..e67094778e7 100644 --- a/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts +++ b/src/vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts @@ -12,11 +12,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { ILogService } from 'vs/platform/log/common/log'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as platform from 'vs/base/common/platform'; import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; -import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalWebWorkerRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { joinPath } from 'vs/base/common/resources'; @@ -30,11 +30,10 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { FileAccess } from 'vs/base/common/network'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { parentOriginHash } from 'vs/workbench/browser/webview'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; export interface IWebWorkerExtensionHostInitData { - readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IWebWorkerExtensionHostDataProvider { @@ -45,7 +44,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost public readonly remoteAuthority = null; public readonly lazyStart: boolean; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onDidExit = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onDidExit.event; @@ -267,7 +266,7 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost private async _createExtHostInitData(): Promise { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -289,14 +288,13 @@ export class WebWorkerExtensionHost extends Disposable implements IExtensionHost name: this._labelService.getWorkspaceLabel(workspace), transient: workspace.transient }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._extensionHostLogsLocation, logFile: this._extensionHostLogFile, - autoStart: initData.autoStart, + autoStart: true, remote: { authority: this._environmentService.remoteAuthority, connectionData: null, diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 0334f7f0b99..fc7e02bdcfc 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -628,12 +628,10 @@ export abstract class AbstractExtensionService extends Disposable implements IEx await Promise.all(promises); } - private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, _toAdd: IExtensionDescription[], _toRemove: ExtensionIdentifier[], removedRunningLocation: Map): Promise { - const toAdd = filterByExtensionHostManager(_toAdd, this._runningLocation, extensionHostManager); - const toRemove = _filterByExtensionHostManager(_toRemove, extId => extId, removedRunningLocation, extensionHostManager); - if (toRemove.length > 0 || toAdd.length > 0) { - await extensionHostManager.deltaExtensions(toAdd, toRemove); - } + private async _updateExtensionsOnExtHost(extensionHostManager: IExtensionHostManager, toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[], removedRunningLocation: Map): Promise { + const myToAdd = filterByExtensionHostManager(toAdd, this._runningLocation, extensionHostManager); + const myToRemove = _filterByExtensionHostManager(toRemove, extId => extId, removedRunningLocation, extensionHostManager); + await extensionHostManager.deltaExtensions({ toRemove, toAdd, myToRemove, myToAdd: myToAdd.map(extension => extension.identifier) }); } public canAddExtension(extension: IExtensionDescription): boolean { diff --git a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts index b3cb9b17813..274bc9c396e 100644 --- a/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionDescriptionRegistry.ts @@ -61,10 +61,8 @@ export class ExtensionDescriptionRegistry { } } - public keepOnly(extensionIds: ExtensionIdentifier[]): void { - const toKeep = new Set(); - extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId))); - this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier))); + public set(extensionDescriptions: IExtensionDescription[]) { + this._extensionDescriptions = extensionDescriptions; this._initialize(); this._onDidChange.fire(undefined); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index e0aff464773..eb4c4437b6a 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -19,13 +19,14 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind, extensionHostKindToString, ExtensionActivationReason, IInternalExtensionService, ExtensionRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { CATEGORIES } from 'vs/workbench/common/actions'; import { Barrier, timeout } from 'vs/base/common/async'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionHostProxy, IResolveAuthorityResult } from 'vs/workbench/services/extensions/common/extensionHostProxy'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -39,7 +40,7 @@ export interface IExtensionHostManager { dispose(): void; ready(): Promise; representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise; containsExtension(extensionId: ExtensionIdentifier): boolean; activate(extension: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; @@ -50,7 +51,7 @@ export interface IExtensionHostManager { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise; - start(enabledExtensionIds: ExtensionIdentifier[]): Promise; + start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise; extensionTestsExecute(): Promise; extensionTestsSendExit(exitCode: number): Promise; setRemoteEnvironment(env: { [key: string]: string | null }): Promise; @@ -398,13 +399,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return proxy.getCanonicalURI(remoteAuthority, uri); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.keepOnly(enabledExtensionIds); - return proxy.startExtensionHost(enabledExtensionIds); + const deltaExtensions = this._extensionHost.extensions.set(allExtensions, myExtensions); + return proxy.startExtensionHost(deltaExtensions); } public async extensionTestsExecute(): Promise { @@ -433,13 +434,13 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise { const proxy = await this._proxy; if (!proxy) { return; } - this._extensionHost.extensions.deltaExtensions(toAdd, toRemove); - return proxy.deltaExtensions(toAdd, toRemove); + this._extensionHost.extensions.delta(extensionsDelta); + return proxy.deltaExtensions(extensionsDelta); } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -468,6 +469,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost private readonly _extensionHost: IExtensionHost; private _startCalled: Barrier; private _actual: ExtensionHostManager | null; + private _lazyStartExtensions: ExtensionHostExtensions | null; public get kind(): ExtensionHostKind { return this._extensionHost.runningLocation.kind; @@ -485,6 +487,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost this.onDidExit = extensionHost.onExit; this._startCalled = new Barrier(); this._actual = null; + this._lazyStartExtensions = null; } private _createActual(reason: string): ExtensionHostManager { @@ -500,7 +503,7 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost return this._actual; } const actual = this._createActual(reason); - await actual.start([]); + await actual.start([], []); return actual; } @@ -513,13 +516,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost public representsRunningLocation(runningLocation: ExtensionRunningLocation): boolean { return this._extensionHost.runningLocation.equals(runningLocation); } - public async deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise { + public async deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise { await this._startCalled.wait(); - const extensionHostAlreadyStarted = Boolean(this._actual); - const shouldStartExtensionHost = (toAdd.length > 0); - if (extensionHostAlreadyStarted || shouldStartExtensionHost) { - const actual = await this._getOrCreateActualAndStart(`contains ${toAdd.length} new extension(s) (installed or enabled): ${toAdd.map(ext => ext.identifier.value)}`); - return actual.deltaExtensions(toAdd, toRemove); + if (this._actual) { + return this._actual.deltaExtensions(extensionsDelta); + } + this._lazyStartExtensions!.delta(extensionsDelta); + if (extensionsDelta.myToAdd.length > 0) { + const actual = this._createActual(`contains ${extensionsDelta.myToAdd.length} new extension(s) (installed or enabled): ${extensionsDelta.myToAdd.map(extId => extId.value)}`); + const { toAdd, myToAdd } = this._lazyStartExtensions!.toDelta(); + actual.start(toAdd, myToAdd); + return; } } public containsExtension(extensionId: ExtensionIdentifier): boolean { @@ -582,15 +589,17 @@ class LazyStartExtensionHostManager extends Disposable implements IExtensionHost } throw new Error(`Cannot resolve canonical URI`); } - public async start(enabledExtensionIds: ExtensionIdentifier[]): Promise { - if (enabledExtensionIds.length > 0) { + public async start(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): Promise { + if (myExtensions.length > 0) { // there are actual extensions, so let's launch the extension host - const actual = this._createActual(`contains ${enabledExtensionIds.length} extension(s): ${enabledExtensionIds.map(extId => extId.value)}.`); - const result = actual.start(enabledExtensionIds); + const actual = this._createActual(`contains ${myExtensions.length} extension(s): ${myExtensions.map(extId => extId.value)}.`); + const result = actual.start(allExtensions, myExtensions); this._startCalled.open(); return result; } - // there are no actual extensions + // there are no actual extensions running, store extensions in `this._lazyStartExtensions` + this._lazyStartExtensions = new ExtensionHostExtensions(); + this._lazyStartExtensions.set(allExtensions, myExtensions); this._startCalled.open(); } public async extensionTestsExecute(): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts index a090dced1df..ca72df6f92f 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProtocol.ts @@ -10,15 +10,21 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; +export interface IExtensionDescriptionDelta { + readonly toRemove: ExtensionIdentifier[]; + readonly toAdd: IExtensionDescription[]; + readonly myToRemove: ExtensionIdentifier[]; + readonly myToAdd: ExtensionIdentifier[]; +} + export interface IExtensionHostInitData { version: string; commit?: string; parentPid: number; environment: IEnvironment; workspace?: IStaticWorkspaceData | null; - resolvedExtensions: ExtensionIdentifier[]; - hostExtensions: ExtensionIdentifier[]; - extensions: IExtensionDescription[]; + allExtensions: IExtensionDescription[]; + myExtensions: ExtensionIdentifier[]; telemetryInfo: ITelemetryInfo; logLevel: LogLevel; logsLocation: URI; diff --git a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts index d021dda0f0d..b528989a727 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostProxy.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostProxy.ts @@ -5,8 +5,9 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRemoteConnectionData, RemoteAuthorityResolverErrorCode, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { ActivationKind, ExtensionActivationReason } from 'vs/workbench/services/extensions/common/extensions'; export interface IResolveAuthorityErrorResult { @@ -31,14 +32,14 @@ export interface IExtensionHostProxy { * Returns `null` if no resolver for `remoteAuthority` is found. */ getCanonicalURI(remoteAuthority: string, uri: URI): Promise; - startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; + startExtensionHost(extensionsDelta: IExtensionDescriptionDelta): Promise; extensionTestsExecute(): Promise; extensionTestsExit(code: number): Promise; activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; setRemoteEnvironment(env: { [key: string]: string | null }): Promise; updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise; - deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise; + deltaExtensions(extensionsDelta: IExtensionDescriptionDelta): Promise; test_latency(n: number): Promise; test_up(b: VSBuffer): Promise; test_down(size: number): Promise; diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index db9df68c5f6..722dba8dd6b 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -13,7 +13,7 @@ import { getExtensionId, getGalleryExtensionId } from 'vs/platform/extensionMana import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { ApiProposalName } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { IV8Profile } from 'vs/platform/profiling/common/profiling'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { IExtensionDescriptionDelta } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; export const nullExtensionDescription = Object.freeze({ identifier: new ExtensionIdentifier('nullExtensionDescription'), @@ -150,10 +150,11 @@ export interface IExtensionHost { readonly remoteAuthority: string | null; readonly lazyStart: boolean; /** - * A collection of extensions that will execute or are executing on this extension host. + * A collection of extensions which includes information about which + * extension will execute or is executing on this extension host. * **NOTE**: this will reflect extensions correctly only after `start()` resolves. */ - readonly extensions: ExtensionDescriptionRegistry; + readonly extensions: ExtensionHostExtensions; readonly onExit: Event<[number, string | null]>; start(): Promise | null; @@ -162,6 +163,201 @@ export interface IExtensionHost { dispose(): void; } +export class ExtensionHostExtensions { + + private _allExtensions: IExtensionDescription[]; + private _myExtensions: ExtensionIdentifier[]; + + constructor() { + this._allExtensions = []; + this._myExtensions = []; + } + + public toDelta(): IExtensionDescriptionDelta { + return { + toRemove: [], + toAdd: this._allExtensions, + myToRemove: [], + myToAdd: this._myExtensions + }; + } + + public set(allExtensions: IExtensionDescription[], myExtensions: ExtensionIdentifier[]): IExtensionDescriptionDelta { + const toRemove: ExtensionIdentifier[] = []; + const toAdd: IExtensionDescription[] = []; + const myToRemove: ExtensionIdentifier[] = []; + const myToAdd: ExtensionIdentifier[] = []; + + const oldExtensionsMap = extensionDescriptionArrayToMap(this._allExtensions); + const newExtensionsMap = extensionDescriptionArrayToMap(allExtensions); + const extensionsAreTheSame = (a: IExtensionDescription, b: IExtensionDescription) => { + return ( + (a.extensionLocation.toString() === b.extensionLocation.toString()) + || (a.isBuiltin === b.isBuiltin) + || (a.isUserBuiltin === b.isUserBuiltin) + || (a.isUnderDevelopment === b.isUnderDevelopment) + ); + }; + + for (const oldExtension of this._allExtensions) { + const newExtension = newExtensionsMap.get(ExtensionIdentifier.toKey(oldExtension.identifier)); + if (!newExtension) { + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + for (const newExtension of allExtensions) { + const oldExtension = oldExtensionsMap.get(ExtensionIdentifier.toKey(newExtension.identifier)); + if (!oldExtension) { + toAdd.push(newExtension); + continue; + } + if (!extensionsAreTheSame(oldExtension, newExtension)) { + // The new extension is different than the old one + // (e.g. maybe it executes in a different location) + toRemove.push(oldExtension.identifier); + oldExtensionsMap.delete(ExtensionIdentifier.toKey(oldExtension.identifier)); + continue; + } + } + + const myOldExtensionsSet = extensionIdentifiersArrayToSet(this._myExtensions); + const myNewExtensionsSet = extensionIdentifiersArrayToSet(myExtensions); + for (const oldExtensionId of this._myExtensions) { + if (!myNewExtensionsSet.has(ExtensionIdentifier.toKey(oldExtensionId))) { + myToRemove.push(oldExtensionId); + } + } + for (const newExtensionId of myExtensions) { + if (!myOldExtensionsSet.has(ExtensionIdentifier.toKey(newExtensionId))) { + myToAdd.push(newExtensionId); + } + } + + const delta = { toRemove, toAdd, myToRemove, myToAdd }; + this.delta(delta); + return delta; + } + + public delta(extensionsDelta: IExtensionDescriptionDelta): void { + const { toRemove, toAdd, myToRemove, myToAdd } = extensionsDelta; + // First handle removals + const toRemoveSet = extensionIdentifiersArrayToSet(toRemove); + const myToRemoveSet = extensionIdentifiersArrayToSet(myToRemove); + for (let i = 0; i < this._allExtensions.length; i++) { + if (toRemoveSet.has(ExtensionIdentifier.toKey(this._allExtensions[i].identifier))) { + this._allExtensions.splice(i, 1); + i--; + } + } + for (let i = 0; i < this._myExtensions.length; i++) { + if (myToRemoveSet.has(ExtensionIdentifier.toKey(this._myExtensions[i]))) { + this._myExtensions.splice(i, 1); + i--; + } + } + // Then handle additions + for (const extension of toAdd) { + this._allExtensions.push(extension); + } + for (const extensionId of myToAdd) { + this._myExtensions.push(extensionId); + } + } + + public containsExtension(extensionId: ExtensionIdentifier): boolean { + for (const myExtensionId of this._myExtensions) { + if (ExtensionIdentifier.equals(myExtensionId, extensionId)) { + return true; + } + } + return false; + } +} + +export class ExtensionIdentifierSet implements Set { + + readonly [Symbol.toStringTag]: string = 'ExtensionIdentifierSet'; + + private readonly _map = new Map(); + private readonly _toKey = ExtensionIdentifier.toKey; + + constructor(values?: Iterable) { + if (values) { + for (const value of values) { + this.add(value); + } + } + } + + get size(): number { + return this._map.size; + } + + add(value: ExtensionIdentifier): this { + this._map.set(this._toKey(value), value); + return this; + } + + clear(): void { + this._map.clear(); + } + + delete(value: ExtensionIdentifier): boolean { + return this._map.delete(this._toKey(value)); + } + + has(value: ExtensionIdentifier): boolean { + return this._map.has(this._toKey(value)); + } + + forEach(callbackfn: (value: ExtensionIdentifier, value2: ExtensionIdentifier, set: Set) => void, thisArg?: any): void { + this._map.forEach(value => callbackfn.call(thisArg, value, value, this)); + } + + *entries(): IterableIterator<[ExtensionIdentifier, ExtensionIdentifier]> { + for (let [_key, value] of this._map) { + yield [value, value]; + } + } + + keys(): IterableIterator { + return this._map.values(); + } + + values(): IterableIterator { + return this._map.values(); + } + + [Symbol.iterator](): IterableIterator { + return this._map.values(); + } +} + +export function extensionIdentifiersArrayToSet(extensionIds: ExtensionIdentifier[]): Set { + const result = new Set(); + for (const extensionId of extensionIds) { + result.add(ExtensionIdentifier.toKey(extensionId)); + } + return result; +} + +function extensionDescriptionArrayToMap(extensions: IExtensionDescription[]): Map { + const result = new Map(); + for (const extension of extensions) { + result.set(ExtensionIdentifier.toKey(extension.identifier), extension); + } + return result; +} + export function isProposedApiEnabled(extension: IExtensionDescription, proposal: ApiProposalName): boolean { if (!extension.enabledApiProposals) { return false; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 0a1e93cded5..62beb7707f3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -19,6 +19,7 @@ export const allApiProposals = Object.freeze({ documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', + extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', externalUriOpener: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.externalUriOpener.d.ts', fileSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.fileSearchProvider.d.ts', findTextInFiles: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.findTextInFiles.d.ts', @@ -30,25 +31,23 @@ export const allApiProposals = Object.freeze({ inputBoxSeverity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts', ipc: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.ipc.d.ts', notebookCellExecutionState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookCellExecutionState.d.ts', - notebookConcatTextDocument: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts', notebookContentProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookContentProvider.d.ts', notebookControllerKind: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookControllerKind.d.ts', notebookDebugOptions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDebugOptions.d.ts', notebookDeprecated: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDeprecated.d.ts', - notebookDocumentEvents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts', notebookEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditor.d.ts', notebookEditorDecorationType: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorDecorationType.d.ts', notebookEditorEdit: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts', notebookLiveShare: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookLiveShare.d.ts', notebookMessaging: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMessaging.d.ts', notebookMime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookMime.d.ts', + notebookProxyController: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts', portsAttributes: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.portsAttributes.d.ts', quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts', resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts', scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts', scmSelectedProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmSelectedProvider.d.ts', scmValidation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmValidation.d.ts', - tabs: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tabs.d.ts', taskPresentationGroup: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.taskPresentationGroup.d.ts', telemetry: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.telemetry.d.ts', terminalDataWriteEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDataWriteEvent.d.ts', diff --git a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts index bd382d7f168..b6eb37647f8 100644 --- a/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts +++ b/src/vs/workbench/services/extensions/common/remoteExtensionHost.ts @@ -25,10 +25,9 @@ import { ISignService } from 'vs/platform/sign/common/sign'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions'; import { createMessageOfType, isMessageOfType, MessageType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; -import { ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionHostExtensions, ExtensionHostLogFileName, IExtensionHost, RemoteRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output'; @@ -39,8 +38,8 @@ export interface IRemoteExtensionHostInitData { readonly extensionHostLogsPath: URI; readonly globalStorageHome: URI; readonly workspaceStorageHome: URI; - readonly extensions: IExtensionDescription[]; readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface IRemoteExtensionHostDataProvider { @@ -52,7 +51,7 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { public readonly remoteAuthority: string; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private _onExit: Emitter<[number, string | null]> = this._register(new Emitter<[number, string | null]>()); public readonly onExit: Event<[number, string | null]> = this._onExit.event; @@ -213,19 +212,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { private async _createExtHostInitData(isExtensionDevelopmentDebug: boolean): Promise { const [telemetryInfo, remoteInitData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); - - // Collect all identifiers for extension ids which can be considered "resolved" - const remoteExtensions = new Set(); - remoteInitData.extensions.forEach((extension) => remoteExtensions.add(ExtensionIdentifier.toKey(extension.identifier.value))); - - const resolvedExtensions = remoteInitData.allExtensions.filter(extension => !extension.main && !extension.browser).map(extension => extension.identifier); - const hostExtensions = ( - remoteInitData.allExtensions - .filter(extension => !remoteExtensions.has(ExtensionIdentifier.toKey(extension.identifier.value))) - .filter(extension => (extension.main || extension.browser) && extension.api === 'none').map(extension => extension.identifier) - ); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(remoteInitData.extensions, []); + const deltaExtensions = this.extensions.set(remoteInitData.allExtensions, remoteInitData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -253,9 +241,8 @@ export class RemoteExtensionHost extends Disposable implements IExtensionHost { authority: this._initDataProvider.remoteAuthority, connectionData: remoteInitData.connectionData }, - resolvedExtensions: resolvedExtensions, - hostExtensions: hostExtensions, - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: remoteInitData.extensionHostLogsPath, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 606ee940774..14be28cc18a 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -162,18 +162,20 @@ export class ExtensionService extends AbstractExtensionService implements IExten // Here we load even extensions that would be disabled by workspace trust const localExtensions = this._checkEnabledAndProposedAPI(await this._scanAllLocalExtensions(), /* ignore workspace trust */true); const runningLocation = this._determineRunningLocation(localExtensions); - const localProcessExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); + const myExtensions = filterByRunningLocation(localExtensions, runningLocation, desiredRunningLocation); return { autoStart: false, - extensions: localProcessExtensions + allExtensions: localExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } else { // restart case const allExtensions = await this.getExtensions(); - const localProcessExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); + const myExtensions = this._filterByRunningLocation(allExtensions, desiredRunningLocation); return { autoStart: true, - extensions: localProcessExtensions + allExtensions: allExtensions, + myExtensions: myExtensions.map(extension => extension.identifier) }; } } @@ -561,8 +563,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionHostLogsPath: remoteEnv.extensionHostLogsPath, globalStorageHome: remoteEnv.globalStorageHome, workspaceStorageHome: remoteEnv.workspaceStorageHome, - extensions: remoteExtensions, allExtensions: this._registry.getAllExtensionDescriptions(), + myExtensions: remoteExtensions.map(extension => extension.identifier), }); } @@ -583,7 +585,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten private _startExtensionHost(extensionHostManager: IExtensionHostManager, _extensions: IExtensionDescription[]): void { const extensions = this._filterByExtensionHostManager(_extensions, extensionHostManager); - extensionHostManager.start(extensions.map(extension => extension.identifier)); + extensionHostManager.start(this._registry.getAllExtensionDescriptions(), extensions.map(extension => extension.identifier)); } public _onExtensionHostExit(code: number): void { diff --git a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts index 2f7e9b944f6..0f7f60bbc7d 100644 --- a/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { Server, Socket, createServer } from 'net'; -import { findFreePort } from 'vs/base/node/ports'; import { createRandomIPCHandle, NodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; import * as nls from 'vs/nls'; import { timeout } from 'vs/base/common/async'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; -import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; @@ -30,11 +29,11 @@ import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { isUntitledWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { MessageType, createMessageOfType, isMessageOfType, IExtensionHostInitData, UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { parseExtensionDevOptions } from '../common/extensionDevOptions'; import { VSBuffer } from 'vs/base/common/buffer'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostLogFileName, LocalProcessRunningLocation, ExtensionHostExtensions } from 'vs/workbench/services/extensions/common/extensions'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -42,13 +41,14 @@ import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import { IExtensionHostProcessOptions, IExtensionHostStarter } from 'vs/platform/extensions/common/extensionHostStarter'; import { SerializedError } from 'vs/base/common/errors'; -import { removeDangerousEnvVariables } from 'vs/base/node/processes'; +import { removeDangerousEnvVariables } from 'vs/base/common/processes'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; +import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export interface ILocalProcessExtensionHostInitData { readonly autoStart: boolean; - readonly extensions: IExtensionDescription[]; + readonly allExtensions: IExtensionDescription[]; + readonly myExtensions: ExtensionIdentifier[]; } export interface ILocalProcessExtensionHostDataProvider { @@ -108,7 +108,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { public readonly remoteAuthority = null; public readonly lazyStart = false; - public readonly extensions = new ExtensionDescriptionRegistry([]); + public readonly extensions = new ExtensionHostExtensions(); private readonly _onExit: Emitter<[number, string]> = new Emitter<[number, string]>(); public readonly onExit: Event<[number, string]> = this._onExit.event; @@ -171,7 +171,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._toDispose.add(this._onExit); this._toDispose.add(this._lifecycleService.onWillShutdown(e => this._onWillShutdown(e))); - this._toDispose.add(this._lifecycleService.onDidShutdown(reason => this.terminate())); + this._toDispose.add(this._lifecycleService.onDidShutdown(() => this.terminate())); this._toDispose.add(this._extensionHostDebugService.onClose(event => { if (this._isExtensionDevHost && this._environmentService.debugExtensionHost.debugId === event.sessionId) { this._nativeHostService.closeWindow(); @@ -182,12 +182,6 @@ export class LocalProcessExtensionHost implements IExtensionHost { this._hostService.reload(); } })); - - const globalExitListener = () => this.terminate(); - process.once('exit', globalExitListener); - this._toDispose.add(toDisposable(() => { - process.removeListener('exit' as 'loaded', globalExitListener); // https://github.com/electron/electron/issues/21475 - })); } public dispose(): void { @@ -380,7 +374,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { } const expected = this._environmentService.debugExtensionHost.port; - const port = await findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); + const port = await this._nativeHostService.findFreePort(expected, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, 2048 /* skip 2048 ports between attempts */); if (!this._isExtensionDevTestFromCli) { if (!port) { @@ -504,7 +498,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { private async _createExtHostInitData(): Promise { const [telemetryInfo, initData] = await Promise.all([this._telemetryService.getTelemetryInfo(), this._initDataProvider.getInitData()]); const workspace = this._contextService.getWorkspace(); - this.extensions.deltaExtensions(initData.extensions, []); + const deltaExtensions = this.extensions.set(initData.allExtensions, initData.myExtensions); return { commit: this._productService.commit, version: this._productService.version, @@ -533,9 +527,8 @@ export class LocalProcessExtensionHost implements IExtensionHost { connectionData: null, isRemote: false }, - resolvedExtensions: [], - hostExtensions: [], - extensions: this.extensions.getAllExtensionDescriptions(), + allExtensions: deltaExtensions.toAdd, + myExtensions: deltaExtensions.myToAdd, telemetryInfo, logLevel: this._logService.getLevel(), logsLocation: this._environmentService.extHostLogsPath, @@ -639,7 +632,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { return withNullAsUndefined(this._inspectPort); } - public terminate(): void { + private terminate(): void { if (this._terminating) { return; } @@ -695,7 +688,7 @@ export class LocalProcessExtensionHost implements IExtensionHost { // to communicate this back to the main side to terminate the debug session if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug && this._environmentService.debugExtensionHost.debugId) { this._extensionHostDebugService.terminateSession(this._environmentService.debugExtensionHost.debugId); - event.join(timeout(100 /* wait a bit for IPC to get delivered */), 'join.extensionDevelopment'); + event.join(timeout(100 /* wait a bit for IPC to get delivered */), { id: 'join.extensionDevelopment', label: nls.localize('join.extensionDevelopment', "Terminating extension debug session") }); } } } diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index cdc63895e27..955ba20bddf 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -34,6 +34,7 @@ import { isUndefined } from 'vs/base/common/types'; import { isTemporaryWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Schemas } from 'vs/base/common/network'; +import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; /** * A workspace to open in the workbench can either be: @@ -285,14 +286,16 @@ export class BrowserHostService extends Disposable implements IHostService { // Same Window: open via editor service in current window if (this.shouldReuse(options, true /* file */)) { - let openables: IPathData[] = []; + let openables: IPathData[] = []; // Support: --goto parameter to open on line/col if (options?.gotoLineMode) { const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path); openables = [{ fileUri: openable.fileUri.with({ path: pathColumnAware.path }), - selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + options: { + selection: !isUndefined(pathColumnAware.line) ? { startLineNumber: pathColumnAware.line, startColumn: pathColumnAware.column || 1 } : undefined + } }]; } else { openables = [openable]; diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts index 2d9166e9ed5..59d8bf7ef8b 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionSimpleWorker.ts @@ -9,7 +9,7 @@ import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; import { IEditorWorkerHost } from 'vs/editor/common/services/editorWorkerHost'; -type RegexpModel = { detect: (inp: string, langBiases: Record) => string | undefined }; +type RegexpModel = { detect: (inp: string, langBiases: Record, supportedLangs?: string[]) => string | undefined }; /** * Called on the worker side @@ -34,7 +34,9 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { private _modelOperations: ModelOperations | undefined; private _loadFailed: boolean = false; - public async detectLanguage(uri: string, langBiases: Record | undefined, preferHistory: boolean): Promise { + private modelIdToCoreId = new Map(); + + public async detectLanguage(uri: string, langBiases: Record | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise { const languages: string[] = []; const confidences: number[] = []; const stopWatch = new StopWatch(true); @@ -43,8 +45,14 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { const neuralResolver = async () => { for await (const language of this.detectLanguagesImpl(documentTextSample)) { - languages.push(language.languageId); - confidences.push(language.confidence); + if (!this.modelIdToCoreId.has(language.languageId)) { + this.modelIdToCoreId.set(language.languageId, await this._host.fhr('getLanguageId', [language.languageId])); + } + const coreId = this.modelIdToCoreId.get(language.languageId); + if (coreId && (!supportedLangs?.length || supportedLangs.includes(coreId))) { + languages.push(coreId); + confidences.push(language.confidence); + } } stopWatch.stop(); @@ -55,15 +63,7 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { return undefined; }; - const historicalResolver = async () => { - if (langBiases) { - const regexpDetection = await this.runRegexpModel(documentTextSample, langBiases); - if (regexpDetection) { - return regexpDetection; - } - } - return undefined; - }; + const historicalResolver = async () => this.runRegexpModel(documentTextSample, langBiases ?? {}, supportedLangs); if (preferHistory) { const history = await historicalResolver(); @@ -112,11 +112,22 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { } } - private async runRegexpModel(content: string, langBiases: Record): Promise { + private async runRegexpModel(content: string, langBiases: Record, supportedLangs?: string[]): Promise { const regexpModel = await this.getRegexpModel(); if (!regexpModel) { return; } - const detected = regexpModel.detect(content, langBiases); + if (supportedLangs?.length) { + // When using supportedLangs, normally computed biases are too extreme. Just use a "bitmask" of sorts. + for (const lang of Object.keys(langBiases)) { + if (supportedLangs.includes(lang)) { + langBiases[lang] = 1; + } else { + langBiases[lang] = 0; + } + } + } + + const detected = regexpModel.detect(content, langBiases, supportedLangs); return detected; } @@ -156,21 +167,21 @@ export class LanguageDetectionSimpleWorker extends EditorSimpleWorker { // For the following languages, we increase the confidence because // these are commonly used languages in VS Code and supported // by the model. - case 'javascript': + case 'js': case 'html': case 'json': - case 'typescript': + case 'ts': case 'css': - case 'python': + case 'py': case 'xml': case 'php': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket1; break; // case 'yaml': // YAML has been know to cause incorrect language detection because the language is pretty simple. We don't want to increase the confidence for this. case 'cpp': - case 'shellscript': + case 'sh': case 'java': - case 'csharp': + case 'cs': case 'c': modelResult.confidence += LanguageDetectionSimpleWorker.positiveConfidenceCorrectionBucket2; break; diff --git a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts index eaa283b67d3..9db520ddcb0 100644 --- a/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts +++ b/src/vs/workbench/services/languageDetection/browser/languageDetectionWorkerServiceImpl.ts @@ -53,7 +53,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet constructor( @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @ILanguageService private readonly _languageService: ILanguageService, + @ILanguageService languageService: ILanguageService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IDiagnosticsService private readonly _diagnosticsService: IDiagnosticsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -68,6 +68,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet this._languageDetectionWorkerClient = new LanguageDetectionWorkerClient( modelService, + languageService, telemetryService, // TODO: See if it's possible to bundle vscode-languagedetection this._environmentService.isBuilt && !isWeb @@ -95,7 +96,7 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet let count = 0; for (const ext of fileExtensions.extensions) { - const langId = this.getLanguageId(ext); + const langId = this._languageDetectionWorkerClient.getLanguageId(ext); if (langId && count < TOP_LANG_COUNTS) { this.workspaceLanguageIds.add(langId); count++; @@ -109,15 +110,6 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return !!languageId && this._configurationService.getValue(LanguageDetectionService.enablementSettingKey, { overrideIdentifier: languageId }); } - private getLanguageId(language: string | undefined): string | undefined { - if (!language) { - return undefined; - } - if (this._languageService.isRegisteredLanguageId(language)) { - return language; - } - return this._languageService.guessLanguageIdByFilepathOrFirstLine(URI.file(`file.${language}`)) ?? undefined; - } private getLanguageBiases(): Record { if (!this.dirtyBiases) { return this.langBiases; } @@ -147,19 +139,14 @@ export class LanguageDetectionService extends Disposable implements ILanguageDet return biases; } - async detectLanguage(resource: URI): Promise { + async detectLanguage(resource: URI, supportedLangs?: string[]): Promise { const useHistory = this._configurationService.getValue(LanguageDetectionService.historyBasedEnablementConfig); const preferHistory = this._configurationService.getValue(LanguageDetectionService.preferHistoryConfig); if (useHistory) { await this.resolveWorkspaceLanguageIds(); } const biases = useHistory ? this.getLanguageBiases() : undefined; - const language = await this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory); - - if (language) { - return this.getLanguageId(language); - } - return undefined; + return this._languageDetectionWorkerClient.detectLanguage(resource, biases, preferHistory, supportedLangs); } private initEditorOpenedListeners(storageService: IStorageService) { @@ -234,6 +221,7 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { constructor( modelService: IModelService, + private readonly _languageService: ILanguageService, private readonly _telemetryService: ITelemetryService, private readonly _indexJsUri: string, private readonly _modelJsonUri: string, @@ -260,6 +248,14 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.workerPromise; } + private _guessLanguageIdByUri(uri: URI): string | undefined { + const guess = this._languageService.guessLanguageIdByFilepathOrFirstLine(uri); + if (guess && guess !== 'unknown') { + return guess; + } + return undefined; + } + override async _getProxy(): Promise { return (await this._getOrCreateLanguageDetectionWorker()).getProxyObject(); } @@ -275,6 +271,8 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this.getWeightsUri(); case 'getRegexpModelUri': return this.getRegexpModelUri(); + case 'getLanguageId': + return this.getLanguageId(args[0]); case 'sendTelemetryEvent': return this.sendTelemetryEvent(args[0], args[1], args[2]); default: @@ -286,6 +284,20 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { return this._indexJsUri; } + getLanguageId(languageIdOrExt: string | undefined) { + if (!languageIdOrExt) { + return undefined; + } + if (this._languageService.isRegisteredLanguageId(languageIdOrExt)) { + return languageIdOrExt; + } + const guessed = this._guessLanguageIdByUri(URI.file(`file.${languageIdOrExt}`)); + if (!guessed || guessed === 'unknown') { + return undefined; + } + return guessed; + } + async getModelJsonUri() { return this._modelJsonUri; } @@ -306,9 +318,15 @@ export class LanguageDetectionWorkerClient extends EditorWorkerClient { }); } - public async detectLanguage(resource: URI, langBiases: Record | undefined, preferHistory: boolean): Promise { + public async detectLanguage(resource: URI, langBiases: Record | undefined, preferHistory: boolean, supportedLangs?: string[]): Promise { + const quickGuess = this._guessLanguageIdByUri(resource); + if (quickGuess) { + return quickGuess; + } + await this._withSyncedResources([resource]); - return (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory); + const modelId = await (await this._getProxy()).detectLanguage(resource.toString(), langBiases, preferHistory, supportedLangs); + return this.getLanguageId(modelId); } } diff --git a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts index 2b725466178..0fe24c9c6fd 100644 --- a/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts +++ b/src/vs/workbench/services/languageDetection/common/languageDetectionWorkerService.ts @@ -19,9 +19,10 @@ export interface ILanguageDetectionService { /** * @param resource The resource to detect the language for. + * @param supportedLangs Optional. When populated, the model will only return languages from the provided list * @returns the language id for the given resource or undefined if the model is not confident enough. */ - detectLanguage(resource: URI): Promise; + detectLanguage(resource: URI, supportedLangs?: string[]): Promise; } //#region Telemetry events diff --git a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts index 6915f6f035d..96c7fcbff3f 100644 --- a/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts @@ -168,9 +168,10 @@ export class BrowserLifecycleService extends AbstractLifecycleService { const logService = this.logService; this._onWillShutdown.fire({ reason: ShutdownReason.QUIT, - token: CancellationToken.None, // Unsupported in web - join(promise, id) { - logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${id})`); + joiners: () => [], // Unsupported in web + token: CancellationToken.None, // Unsupported in web + join(promise, joiner) { + logService.error(`[lifecycle] Long running operations during shutdown are unsupported in the web (id: ${joiner.id})`); }, force: () => { /* No-Op in web */ }, }); diff --git a/src/vs/workbench/services/lifecycle/common/lifecycle.ts b/src/vs/workbench/services/lifecycle/common/lifecycle.ts index 199577ebfde..a8c2c9e2cd6 100644 --- a/src/vs/workbench/services/lifecycle/common/lifecycle.ts +++ b/src/vs/workbench/services/lifecycle/common/lifecycle.ts @@ -65,6 +65,11 @@ export interface BeforeShutdownErrorEvent { readonly error: Error; } +export interface IWillShutdownEventJoiner { + id: string; + label: string; +} + /** * An event that is send out when the window closes. Clients have a chance to join the closing * by providing a promise from the join method. Returning a promise is useful in cases of long @@ -90,10 +95,15 @@ export interface WillShutdownEvent { * Allows to join the shutdown. The promise can be a long running operation but it * will block the application from closing. * - * @param id to identify the join operation in case it takes very long or never + * @param joiner to identify the join operation in case it takes very long or never * completes. */ - join(promise: Promise, id: string): void; + join(promise: Promise, joiner: IWillShutdownEventJoiner): void; + + /** + * Allows to access the joiners that have not finished joining this event. + */ + joiners(): IWillShutdownEventJoiner[]; /** * Allows to enforce the shutdown, even when there are @@ -170,7 +180,7 @@ export const enum LifecyclePhase { Eventually = 4 } -export function LifecyclePhaseToString(phase: LifecyclePhase) { +export function LifecyclePhaseToString(phase: LifecyclePhase): string { switch (phase) { case LifecyclePhase.Starting: return 'Starting'; case LifecyclePhase.Ready: return 'Ready'; diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index a02dbc84a53..9ad9e92efe9 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; -import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ShutdownReason, ILifecycleService, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService } from 'vs/platform/log/common/log'; @@ -155,18 +155,19 @@ export class NativeLifecycleService extends AbstractLifecycleService { protected async handleWillShutdown(reason: ShutdownReason): Promise { const joiners: Promise[] = []; - const pendingJoiners = new Set(); + const pendingJoiners = new Set(); const cts = new CancellationTokenSource(); this._onWillShutdown.fire({ reason, token: cts.token, - join(promise, id) { + joiners: () => Array.from(pendingJoiners.values()), + join(promise, joiner) { joiners.push(promise); // Track promise completion - pendingJoiners.add(id); - promise.finally(() => pendingJoiners.delete(id)); + pendingJoiners.add(joiner); + promise.finally(() => pendingJoiners.delete(joiner)); }, force: () => { cts.dispose(true); @@ -174,7 +175,7 @@ export class NativeLifecycleService extends AbstractLifecycleService { }); const longRunningWillShutdownWarning = disposableTimeout(() => { - this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).join(', ')}`); + this.logService.warn(`[lifecycle] onWillShutdown is taking a long time, pending operations: ${Array.from(pendingJoiners).map(joiner => joiner.id).join(', ')}`); }, NativeLifecycleService.WILL_SHUTDOWN_WARNING_DELAY); try { diff --git a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts index 63cd2106537..4edcf36737f 100644 --- a/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts +++ b/src/vs/workbench/services/lifecycle/test/electron-browser/lifecycleService.test.ts @@ -132,7 +132,7 @@ suite('Lifecycleservice', function () { joinCalled = true; resolve(); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); @@ -148,7 +148,7 @@ suite('Lifecycleservice', function () { joinCalled = true; reject(new Error('Fail')); - }), 'test'); + }), { id: 'test', label: 'test' }); }); await lifecycleService.handleWillShutdown(ShutdownReason.QUIT); diff --git a/src/vs/workbench/services/output/common/output.ts b/src/vs/workbench/services/output/common/output.ts index f12fa430d7a..6adc8948c82 100644 --- a/src/vs/workbench/services/output/common/output.ts +++ b/src/vs/workbench/services/output/common/output.ts @@ -6,6 +6,144 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +/** + * Mime type used by the output editor. + */ +export const OUTPUT_MIME = 'text/x-code-output'; + +/** + * Output resource scheme. + */ +export const OUTPUT_SCHEME = 'output'; + +/** + * Id used by the output editor. + */ +export const OUTPUT_MODE_ID = 'Log'; + +/** + * Mime type used by the log output editor. + */ +export const LOG_MIME = 'text/x-code-log-output'; + +/** + * Log resource scheme. + */ +export const LOG_SCHEME = 'log'; + +/** + * Id used by the log output editor. + */ +export const LOG_MODE_ID = 'log'; + +/** + * Output view id + */ +export const OUTPUT_VIEW_ID = 'workbench.panel.output'; + +export const OUTPUT_SERVICE_ID = 'outputService'; + +export const MAX_OUTPUT_LENGTH = 10000 /* Max. number of output lines to show in output */ * 100 /* Guestimated chars per line */; + +export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); + +export const CONTEXT_ACTIVE_LOG_OUTPUT = new RawContextKey('activeLogOutput', false); + +export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); + +export const IOutputService = createDecorator(OUTPUT_SERVICE_ID); + +/** + * The output service to manage output from the various processes running. + */ +export interface IOutputService { + readonly _serviceBrand: undefined; + + /** + * Given the channel id returns the output channel instance. + * Channel should be first registered via OutputChannelRegistry. + */ + getChannel(id: string): IOutputChannel | undefined; + + /** + * Given the channel id returns the registered output channel descriptor. + */ + getChannelDescriptor(id: string): IOutputChannelDescriptor | undefined; + + /** + * Returns an array of all known output channels descriptors. + */ + getChannelDescriptors(): IOutputChannelDescriptor[]; + + /** + * Returns the currently active channel. + * Only one channel can be active at a given moment. + */ + getActiveChannel(): IOutputChannel | undefined; + + /** + * Show the channel with the passed id. + */ + showChannel(id: string, preserveFocus?: boolean): Promise; + + /** + * Allows to register on active output channel change. + */ + onActiveOutputChannel: Event; +} + +export enum OutputChannelUpdateMode { + Append = 1, + Replace, + Clear +} + +export interface IOutputChannel { + + /** + * Identifier of the output channel. + */ + id: string; + + /** + * Label of the output channel to be displayed to the user. + */ + label: string; + + /** + * URI of the output channel. + */ + uri: URI; + + /** + * Appends output to the channel. + */ + append(output: string): void; + + /** + * Clears all received output for this channel. + */ + clear(): void; + + /** + * Replaces the content of the channel with given output + */ + replace(output: string): void; + + /** + * Update the channel. + */ + update(mode: OutputChannelUpdateMode.Append): void; + update(mode: OutputChannelUpdateMode, till: number): void; + + /** + * Disposes the output channel. + */ + dispose(): void; +} export const Extensions = { OutputChannels: 'workbench.contributions.outputChannels' diff --git a/src/vs/workbench/services/preferences/browser/preferencesService.ts b/src/vs/workbench/services/preferences/browser/preferencesService.ts index eb5737fa9af..555a3a7ee20 100644 --- a/src/vs/workbench/services/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/services/preferences/browser/preferencesService.ts @@ -17,7 +17,6 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Extensions, getDefaultValue, IConfigurationRegistry, OVERRIDE_PROPERTY_REGEX } from 'vs/platform/configuration/common/configurationRegistry'; import { EditorResolution } from 'vs/platform/editor/common/editor'; @@ -46,6 +45,7 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA import { ITextEditorService } from 'vs/workbench/services/textfile/common/textEditorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { isArray, isObject } from 'vs/base/common/types'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; const emptyEditableSettingsContent = '{\n}'; @@ -76,7 +76,6 @@ export class PreferencesService extends Disposable implements IPreferencesServic @ILanguageService private readonly languageService: ILanguageService, @ILabelService private readonly labelService: ILabelService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, - @ICommandService private readonly commandService: ICommandService, @ITextEditorService private readonly textEditorService: ITextEditorService ) { super(); @@ -542,7 +541,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic codeEditor.revealPositionNearTop(position); codeEditor.focus(); if (edit) { - await this.commandService.executeCommand('editor.action.triggerSuggest'); + SuggestController.get(codeEditor)?.triggerSuggest(); } } } diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index 27f646183f4..5b8449ffd83 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -74,7 +74,7 @@ export interface RelativePattern { * (like `** /*.{ts,js}` without space before / or `*.{ts,js}`) or a [relative pattern](#RelativePattern). * * Glob patterns can have the following syntax: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `** /*.{ts,js}` without space before / matches all TypeScript and JavaScript files) diff --git a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts index 2d2d4d50253..6590bc91938 100644 --- a/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts +++ b/src/vs/workbench/services/sharedProcess/electron-sandbox/sharedProcessService.ts @@ -42,10 +42,10 @@ export class SharedProcessService extends Disposable implements ISharedProcessSe // Acquire a message port connected to the shared process mark('code/willConnectSharedProcess'); - this.logService.info('Renderer->SharedProcess#connect: before acquirePort'); + this.logService.trace('Renderer->SharedProcess#connect: before acquirePort'); const port = await acquirePort('vscode:createSharedProcessMessageChannel', 'vscode:createSharedProcessMessageChannelResult'); mark('code/didConnectSharedProcess'); - this.logService.info('Renderer->SharedProcess#connect: connection established'); + this.logService.trace('Renderer->SharedProcess#connect: connection established'); return this._register(new MessagePortClient(port, `window:${this.windowId}`)); } diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 448f2aa30b9..4b4d5ead47c 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -5,6 +5,7 @@ import type { ApplicationInsights } from '@microsoft/applicationinsights-web'; import { Disposable } from 'vs/base/common/lifecycle'; +import { IObservableValue } from 'vs/base/common/observableValue'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILoggerService } from 'vs/platform/log/common/log'; @@ -138,7 +139,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index ca23c70a78f..a91c4a23be9 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -17,6 +17,7 @@ import { TelemetryService as BaseTelemetryService, ITelemetryServiceConfig } fro import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { IFileService } from 'vs/platform/files/common/files'; +import { IObservableValue } from 'vs/base/common/observableValue'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -56,7 +57,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.setExperimentProperty(name, value); } - get telemetryLevel(): TelemetryLevel { + get telemetryLevel(): IObservableValue { return this.impl.telemetryLevel; } diff --git a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts index d4f8852280b..94cd05ed2e3 100644 --- a/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; import { ITextFileService, ITextFileStreamContent, ITextFileContent, IReadTextFileOptions, TextFileEditorModelState, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -63,7 +64,7 @@ export class NativeTextFileService extends AbstractTextFileService { private registerListeners(): void { // Lifecycle - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.textFiles')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.textFiles', label: localize('join.textFiles', "Saving text files") })); } private async onWillShutdown(): Promise { diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 6f8986a8963..901f92d9f3e 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -129,7 +129,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this)); this.fileIconThemeRegistry = new ThemeRegistry(fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme); this.fileIconThemeLoader = new FileIconThemeLoader(extensionResourceLoaderService, languageService); - this.onFileIconThemeChange = new Emitter(); + this.onFileIconThemeChange = new Emitter({ leakWarningThreshold: 400 }); this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); this.fileIconThemeSequencer = new Sequencer(); diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index b92776c6c5a..d7c4f83dcc0 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCopy, IStoredFileWorkingCopyModel, IStoredFileWorkingCopyModelFactory, IStoredFileWorkingCopyResolveOptions, IStoredFileWorkingCopySaveEvent as IBaseStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; @@ -207,7 +208,7 @@ export class StoredFileWorkingCopyManager // Lifecycle this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(), 'veto.fileWorkingCopyManager')); - this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), 'join.fileWorkingCopyManager')); + this.lifecycleService.onWillShutdown(event => event.join(this.onWillShutdown(), { id: 'join.fileWorkingCopyManager', label: localize('join.fileWorkingCopyManager', "Saving working copies") })); } private onBeforeShutdown(): boolean { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts index c38a73c7699..667440f7070 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyHistoryService.ts @@ -76,7 +76,10 @@ export class WorkingCopyHistoryModel { private historyEntriesNameMatcher: RegExp | undefined = undefined; - private shouldStore: boolean = false; + private versionId = 0; + private storedVersionId = this.versionId; + + private readonly storeLimiter = new Limiter(1); constructor( workingCopyResource: URI, @@ -170,8 +173,8 @@ export class WorkingCopyHistoryModel { }; this.entries.push(entry); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryAddedEmitter.fire({ entry }); @@ -188,8 +191,8 @@ export class WorkingCopyHistoryModel { // Update entry entry.timestamp = timestamp; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryReplacedEmitter.fire({ entry }); @@ -217,8 +220,8 @@ export class WorkingCopyHistoryModel { // Remove from model this.entries.splice(index, 1); - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryRemovedEmitter.fire({ entry }); @@ -248,8 +251,8 @@ export class WorkingCopyHistoryModel { // Update entry entry.source = properties.source; - // Mark as in need to be stored to disk - this.shouldStore = true; + // Update version ID of model to use for storing later + this.versionId++; // Events this.entryChangedEmitter.fire({ entry }); @@ -384,12 +387,29 @@ export class WorkingCopyHistoryModel { } async store(token: CancellationToken): Promise { - const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); - - if (!this.shouldStore) { - return; // fast return to avoid disk access when nothing changed + if (!this.shouldStore()) { + return; } + // Use a `Limiter` to prevent multiple `store` operations + // potentially running at the same time + + await this.storeLimiter.queue(async () => { + if (token.isCancellationRequested || !this.shouldStore()) { + return; + } + + return this.doStore(token); + }); + } + + private shouldStore(): boolean { + return this.storedVersionId !== this.versionId; + } + + private async doStore(token: CancellationToken): Promise { + const historyEntriesFolder = assertIsDefined(this.historyEntriesFolder); + // Make sure to await resolving when persisting await this.resolveEntriesOnce(); @@ -401,6 +421,7 @@ export class WorkingCopyHistoryModel { await this.cleanUpEntries(); // Without entries, remove the history folder + const storedVersion = this.versionId; if (this.entries.length === 0) { try { await this.fileService.del(historyEntriesFolder, { recursive: true }); @@ -414,8 +435,8 @@ export class WorkingCopyHistoryModel { await this.writeEntriesFile(); } - // Mark as being up to date on disk - this.shouldStore = false; + // Mark as stored version + this.storedVersionId = storedVersion; } private async cleanUpEntries(): Promise { diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts index d4c07c0f67a..d9fd62ac9b4 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -33,7 +34,7 @@ export class NativeWorkingCopyBackupService extends WorkingCopyBackupService { // Lifecycle: ensure to prolong the shutdown for as long // as pending backup operations have not finished yet. // Otherwise, we risk writing partial backups to disk. - this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), 'join.workingCopyBackups')); + this.lifecycleService.onWillShutdown(event => event.join(this.joinBackups(), { id: 'join.workingCopyBackups', label: localize('join.workingCopyBackups', "Backup working copies") })); } } diff --git a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts index a252865a59a..55167103df8 100644 --- a/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService.ts @@ -3,7 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Limiter } from 'vs/base/common/async'; +import { localize } from 'vs/nls'; +import { Event } from 'vs/base/common/event'; +import { Limiter, RunOnceScheduler } from 'vs/base/common/async'; import { ILifecycleService, WillShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -15,11 +17,17 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IWorkingCopyHistoryModelOptions, WorkingCopyHistoryService } from 'vs/workbench/services/workingCopy/common/workingCopyHistoryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkingCopyHistoryService, MAX_PARALLEL_HISTORY_IO_OPS } from 'vs/workbench/services/workingCopy/common/workingCopyHistory'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { + private static readonly STORE_ALL_INTERVAL = 5 * 60 * 1000; // 5min + private readonly isRemotelyStored = typeof this.environmentService.remoteAuthority === 'string'; + private readonly storeAllCts = this._register(new CancellationTokenSource()); + private readonly storeAllScheduler = this._register(new RunOnceScheduler(() => this.storeAll(this.storeAllCts.token), NativeWorkingCopyHistoryService.STORE_ALL_INTERVAL)); + constructor( @IFileService fileService: IFileService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, @@ -32,9 +40,17 @@ export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { ) { super(fileService, remoteAgentService, environmentService, uriIdentityService, labelService, logService, configurationService); - // When local, delay the flushing until shutdown + this.registerListeners(); + } + + private registerListeners(): void { if (!this.isRemotelyStored) { + + // Local: persist all on shutdown this.lifecycleService.onWillShutdown(e => this.onWillShutdown(e)); + + // Local: schedule persist on change + this._register(Event.any(this.onDidAddEntry, this.onDidChangeEntry, this.onDidReplaceEntry, this.onDidRemoveEntry)(() => this.onDidChangeModels())); } } @@ -44,28 +60,40 @@ export class NativeWorkingCopyHistoryService extends WorkingCopyHistoryService { private onWillShutdown(e: WillShutdownEvent): void { - // Prolong shutdown for orderly model shutdown - e.join((async () => { - const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); - const promises = []; + // Dispose the scheduler... + this.storeAllScheduler.dispose(); + this.storeAllCts.dispose(true); - const models = Array.from(this.models.values()); - for (const model of models) { - promises.push(limiter.queue(async () => { - if (e.token.isCancellationRequested) { - return; - } + // ...because we now explicitly store all models + e.join(this.storeAll(e.token), { id: 'join.workingCopyHistory', label: localize('join.workingCopyHistory', "Saving local history") }); + } - try { - await model.store(e.token); - } catch (error) { - this.logService.trace(error); - } - })); - } + private onDidChangeModels(): void { + if (!this.storeAllScheduler.isScheduled()) { + this.storeAllScheduler.schedule(); + } + } - await Promise.all(promises); - })(), 'join.workingCopyHistory'); + private async storeAll(token: CancellationToken): Promise { + const limiter = new Limiter(MAX_PARALLEL_HISTORY_IO_OPS); + const promises = []; + + const models = Array.from(this.models.values()); + for (const model of models) { + promises.push(limiter.queue(async () => { + if (token.isCancellationRequested) { + return; + } + + try { + await model.store(token); + } catch (error) { + this.logService.trace(error); + } + })); + } + + await Promise.all(promises); } } diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 49d6c42d4a8..e5039e9bc04 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -21,8 +21,7 @@ import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isEqualAuthority } from 'vs/base/common/resources'; -import { ILogService } from 'vs/platform/log/common/log'; -import { isCI, isWeb } from 'vs/base/common/platform'; +import { isWeb } from 'vs/base/common/platform'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; export const WORKSPACE_TRUST_STARTUP_PROMPT = 'security.workspace.trust.startupPrompt'; @@ -119,8 +118,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService, - @ILogService protected readonly _logService: ILogService, + @IWorkspaceTrustEnablementService private readonly workspaceTrustEnablementService: IWorkspaceTrustEnablementService ) { super(); @@ -147,10 +145,6 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork //#region initialize private initializeWorkspaceTrust(): void { - if (isCI) { - this._logService.info(`[WT] Enter initializeWorkspaceTrust()...`); - } - // Resolve canonical Uris this.resolveCanonicalUris() .then(async () => { @@ -158,15 +152,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); }) .finally(() => { - if (isCI) { - this._logService.info(`[WT] Open workspaceResolved gate...`); - } this._workspaceResolvedPromiseResolve(); if (!this.environmentService.remoteAuthority) { - if (isCI) { - this._logService.info(`[WT] Open workspaceTrustInitialized gate...`); - } this._workspaceTrustInitializedPromiseResolve(); } }); @@ -211,33 +199,21 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async getCanonicalUri(uri: URI): Promise { - if (isCI) { - this._logService.info('[WT] Enter getCanonicalUri()...'); - } - + let canonicalUri = uri; if (this.environmentService.remoteAuthority && uri.scheme === Schemas.vscodeRemote) { - if (isCI) { - this._logService.info('[WT] Return this.remoteAuthorityResolverService.getCanonicalURI(uri)...'); - } - - return this.remoteAuthorityResolverService.getCanonicalURI(uri); - } - - if (uri.scheme === 'vscode-vfs') { + canonicalUri = await this.remoteAuthorityResolverService.getCanonicalURI(uri); + } else if (uri.scheme === 'vscode-vfs') { const index = uri.authority.indexOf('+'); if (index !== -1) { - return uri.with({ authority: uri.authority.substr(0, index) }); + canonicalUri = uri.with({ authority: uri.authority.substr(0, index) }); } } - return uri; + // ignore query and fragent section of uris always + return canonicalUri.with({ query: null, fragment: null }); } private async resolveCanonicalUris(): Promise { - if (isCI) { - this._logService.info('[WT] Enter resolveCanonicalUris()...'); - } - // Open editors const filesToOpen: IPath[] = []; if (this.environmentService.filesToOpenOrCreate) { @@ -255,32 +231,16 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._canonicalStartupFiles.push(...canonicalFilesToOpen.filter(uri => this._canonicalStartupFiles.every(u => !this.uriIdentityService.extUri.isEqual(uri, u)))); } - if (isCI) { - this._logService.info('[WT] Done processing open editors...'); - } - // Workspace const workspaceUris = this.workspaceService.getWorkspace().folders.map(f => f.uri); const canonicalWorkspaceFolders = await Promise.all(workspaceUris.map(uri => this.getCanonicalUri(uri))); - if (isCI) { - this._logService.info('[WT] Done processing workspace folders...'); - } - let canonicalWorkspaceConfiguration = this.workspaceService.getWorkspace().configuration; if (canonicalWorkspaceConfiguration && isSavedWorkspace(canonicalWorkspaceConfiguration, this.environmentService)) { canonicalWorkspaceConfiguration = await this.getCanonicalUri(canonicalWorkspaceConfiguration); } - if (isCI) { - this._logService.info('[WT] Done processing workspace configuration...'); - } - this._canonicalWorkspace = new CanonicalWorkspace(this.workspaceService.getWorkspace(), canonicalWorkspaceFolders, canonicalWorkspaceConfiguration); - - if (isCI) { - this._logService.info('[WT] Exit resolveCanonicalUris()...'); - } } private loadTrustInfo(): IWorkspaceTrustInfo { @@ -362,16 +322,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } private async updateWorkspaceTrust(trusted?: boolean): Promise { - if (isCI) { - this._logService.info(`[WT] Enter updateWorkspaceTrust()...`); - } - if (!this.workspaceTrustEnablementService.isWorkspaceTrustEnabled()) { - if (isCI) { - this._logService.info(`[WT] Workspace trust is disabled.`); - this._logService.info(`[WT] Exit updateWorkspaceTrust()...`); - } - return; } diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index da388396897..cb8808efc50 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { FileService } from 'vs/platform/files/common/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { ILogService, NullLogService } from 'vs/platform/log/common/log'; +import { NullLogService } from 'vs/platform/log/common/log'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -28,7 +28,6 @@ suite('Workspace Trust', () => { let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; let environmentService: IWorkbenchEnvironmentService; - let logService: ILogService; setup(async () => { instantiationService = new TestInstantiationService(); @@ -39,10 +38,7 @@ suite('Workspace Trust', () => { environmentService = {} as IWorkbenchEnvironmentService; instantiationService.stub(IWorkbenchEnvironmentService, environmentService); - logService = new NullLogService(); - instantiationService.stub(ILogService, logService); - - instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(logService))); + instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 3dcb9b71147..b4690df3ec9 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -21,7 +21,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorOptions, IResourceEditorInput, IEditorModel, IResourceEditorInputIdentifier, ITextResourceEditorInput, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; -import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent, BeforeShutdownErrorEvent, InternalBeforeShutdownEvent, IWillShutdownEventJoiner } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { FileOperationEvent, IFileService, IFileStat, IFileStatResult, FileChangesEvent, IResolveFileOptions, ICreateFileOptions, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, IFileDeleteOptions, IFileOverwriteOptions, IFileWriteOptions, IFileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions, IReadFileOptions, IFileContent, IFileStreamContent, FileOperationError, IFileSystemProviderWithFileReadStreamCapability, IFileReadStreamOptions, IReadFileStreamOptions, IFileSystemProviderCapabilitiesChangeEvent, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/model'; @@ -1227,6 +1227,7 @@ export class TestLifecycleService implements ILifecycleService { join: p => { this.shutdownJoiners.push(p); }, + joiners: () => [], force: () => { /* No-Op in tests */ }, token: CancellationToken.None, reason @@ -1261,10 +1262,11 @@ export class TestBeforeShutdownEvent implements InternalBeforeShutdownEvent { export class TestWillShutdownEvent implements WillShutdownEvent { value: Promise[] = []; + joiners = () => []; reason = ShutdownReason.CLOSE; token = CancellationToken.None; - join(promise: Promise, id: string): void { + join(promise: Promise, joiner: IWillShutdownEventJoiner): void { this.value.push(promise); } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index ba5b81b1d97..2b52b2c6c27 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -245,6 +245,7 @@ export class TestNativeHostService implements INativeHostService { async toggleDevTools(): Promise { } async toggleSharedProcessWindow(): Promise { } async resolveProxy(url: string): Promise { return undefined; } + async findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride?: number): Promise { return -1; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise { return ''; } async writeClipboardText(text: string, type?: 'selection' | 'clipboard' | undefined): Promise { } async readClipboardFindText(): Promise { return ''; } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index cd19ac8ad76..bf87db19d53 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -304,6 +304,9 @@ import 'vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution'; import 'vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline'; import 'vs/workbench/contrib/outline/browser/outline.contribution'; +// Language Detection +import 'vs/workbench/contrib/languageDetection/browser/languageDetection.contribution'; + // Language Status import 'vs/workbench/contrib/languageStatus/browser/languageStatus.contribution'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 4376a8c5e27..c58c29708e7 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -181,7 +181,6 @@ import type { IUpdateProvider, IUpdate } from 'vs/workbench/services/update/brow // eslint-disable-next-line no-duplicate-imports import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; - export { // Factory diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 57af873b92d..8a6f060c429 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1924,12 +1924,12 @@ declare module 'vscode' { title?: string; /** - * The value to prefill in the input box. + * The value to pre-fill in the input box. */ value?: string; /** - * Selection of the prefilled {@linkcode InputBoxOptions.value value}. Defined as tuple of two number where the + * Selection of the pre-filled {@linkcode InputBoxOptions.value value}. Defined as tuple of two number where the * first is the inclusive start index and the second the exclusive end index. When `undefined` the whole * word will be selected, when empty (start equals end) only the cursor will be set, * otherwise the defined range will be selected. @@ -2032,7 +2032,7 @@ declare module 'vscode' { * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}. * * Glob patterns can have the following syntax: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) @@ -9044,6 +9044,11 @@ declare module 'vscode' { */ export namespace window { + /** + * Represents the grid widget within the main editor area + */ + export const tabGroups: TabGroups; + /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed @@ -9420,7 +9425,7 @@ declare module 'vscode' { * If language id is not provided, then **Log** is used as default language id. * * You can access the visible or active output channel as a {@link TextDocument text document} from {@link window.visibleTextEditors visible editors} or {@link window.activeTextEditor active editor} - * and use the langage id to contribute language features like syntax coloring, code lens etc., + * and use the language id to contribute language features like syntax coloring, code lens etc., * * @param name Human-readable string which will be used to represent the channel in the UI. * @param languageId The identifier of the language associated with the channel. @@ -9437,7 +9442,7 @@ declare module 'vscode' { * * @return New webview panel. */ - export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { viewColumn: ViewColumn; preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; + export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { readonly viewColumn: ViewColumn; readonly preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; /** * Set a message to the status bar. This is a short hand for the more powerful @@ -11504,7 +11509,7 @@ declare module 'vscode' { * vscode.workspace.createFileSystemWatcher('**​/*.js')); * ``` * - * *Note:* the array of workspace folders can be empy if no workspace is opened (empty window). + * *Note:* the array of workspace folders can be empty if no workspace is opened (empty window). * * #### Out of workspace file watching * @@ -11703,7 +11708,7 @@ declare module 'vscode' { * {@linkcode notebook.onDidCloseNotebookDocument onDidCloseNotebookDocument}-event can occur at any time after. * * *Note* that opening a notebook does not show a notebook editor. This function only returns a notebook document which - * can be showns in a notebook editor but it can also be used for other things. + * can be shown in a notebook editor but it can also be used for other things. * * @param uri The resource to open. * @returns A promise that resolves to a {@link NotebookDocument notebook} @@ -11721,6 +11726,16 @@ declare module 'vscode' { */ export function openNotebookDocument(notebookType: string, content?: NotebookData): Thenable; + /** + * An event that is emitted when a {@link NotebookDocument notebook} has changed. + */ + export const onDidChangeNotebookDocument: Event; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is saved. + */ + export const onDidSaveNotebookDocument: Event; + /** * Register a {@link NotebookSerializer notebook serializer}. * @@ -11728,7 +11743,7 @@ declare module 'vscode' { * the `onNotebook:` activation event, and extensions must register their serializer in return. * * @param notebookType A notebook. - * @param serializer A notebook serialzier. + * @param serializer A notebook serializer. * @param options Optional context options that define what parts of a notebook should be persisted * @return A {@link Disposable} that unregisters this serializer when being disposed. */ @@ -12440,6 +12455,39 @@ declare module 'vscode' { } + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + * Additional properties of the NotebookEditor are available in the proposed + * API, which will be finalized later. + */ + export interface NotebookEditor { + + } + + /** + * Renderer messaging is used to communicate with a single renderer. It's returned from {@link notebooks.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * An event that fires when a message is received from a renderer. + */ + readonly onDidReceiveMessage: Event<{ + readonly editor: NotebookEditor; + readonly message: any; + }>; + + /** + * Send a message to one or all renderer. + * + * @param message Message to send + * @param editor Editor to target with the message. If not provided, the + * message is sent to all renderers. + * @returns a boolean indicating whether the message was successfully + * delivered to any renderer. + */ + postMessage(message: any, editor?: NotebookEditor): Thenable; + } + /** * A notebook cell kind. */ @@ -12503,39 +12551,6 @@ declare module 'vscode' { readonly executionSummary: NotebookCellExecutionSummary | undefined; } - /** - * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. - * Additional properties of the NotebookEditor are available in the proposed - * API, which will be finalized later. - */ - export interface NotebookEditor { - - } - - /** - * Renderer messaging is used to communicate with a single renderer. It's returned from {@link notebooks.createRendererMessaging}. - */ - export interface NotebookRendererMessaging { - /** - * An event that fires when a message is received from a renderer. - */ - readonly onDidReceiveMessage: Event<{ - readonly editor: NotebookEditor; - readonly message: any; - }>; - - /** - * Send a message to one or all renderer. - * - * @param message Message to send - * @param editor Editor to target with the message. If not provided, the - * message is sent to all renderers. - * @returns a boolean indicating whether the message was successfully - * delivered to any renderer. - */ - postMessage(message: any, editor?: NotebookEditor): Thenable; - } - /** * Represents a notebook which itself is a sequence of {@link NotebookCell code or markup cells}. Notebook documents are * created from {@link NotebookData notebook data}. @@ -12615,6 +12630,94 @@ declare module 'vscode' { save(): Thenable; } + /** + * Describes a change to a notebook cell. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentCellChange { + + /** + * The affected notebook. + */ + readonly cell: NotebookCell; + + /** + * The document of the cell or `undefined` when it did not change. + * + * *Note* that you should use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event + * for detailed change information, like what edits have been performed. + */ + readonly document: TextDocument | undefined; + + /** + * The new metadata of the cell or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * The new outputs of the cell or `undefined` when they did not change. + */ + readonly outputs: readonly NotebookCellOutput[] | undefined; + + /** + * The new execution summary of the cell or `undefined` when it did not change. + */ + readonly executionSummary: NotebookCellExecutionSummary | undefined; + } + + /** + * Describes a structural change to a notebook document, e.g newly added and removed cells. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentContentChange { + + /** + * The range at which cells have been either added or removed. + * + * Note that no cells have been {@link NotebookDocumentContentChange.removedCells removed} + * when this range is {@link NotebookRange.isEmpty empty}. + */ + readonly range: NotebookRange; + + /** + * Cells that have been added to the document. + */ + readonly addedCells: readonly NotebookCell[]; + + /** + * Cells that have been removed from the document. + */ + readonly removedCells: readonly NotebookCell[]; + } + + /** + * An event describing a transactional {@link NotebookDocument notebook} change. + */ + export interface NotebookDocumentChangeEvent { + + /** + * The affected notebook. + */ + readonly notebook: NotebookDocument; + + /** + * The new metadata of the notebook or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * An array of content changes describing added or removed {@link NotebookCell cells}. + */ + readonly contentChanges: readonly NotebookDocumentContentChange[]; + + /** + * An array of {@link NotebookDocumentCellChange cell changes}. + */ + readonly cellChanges: readonly NotebookDocumentCellChange[]; + } + /** * The summary of a notebook cell execution. */ @@ -14289,7 +14392,7 @@ declare module 'vscode' { /** * The range the comment thread is located within the document. The thread icon will be shown - * at the first line of the range. + * at the last line of the range. */ range: Range; @@ -14542,8 +14645,6 @@ declare module 'vscode' { export function createCommentController(id: string, label: string): CommentController; } - //#endregion - /** * Represents a session of a currently logged in user. */ @@ -15349,6 +15450,294 @@ declare module 'vscode' { */ constructor(message: string | MarkdownString); } + + /** + * The tab represents a single text based resource + */ + export class TabInputText { + /** + * The uri represented by the tab. + */ + readonly uri: Uri; + /** + * Constructs a text tab input with the given URI. + * @param uri The URI of the tab. + */ + constructor(uri: Uri); + } + + /** + * The tab represents two text based resources + * being rendered as a diff. + */ + export class TabInputTextDiff { + /** + * The uri of the original text resource. + */ + readonly original: Uri; + /** + * The uri of the modified text resource. + */ + readonly modified: Uri; + /** + * Constructs a new text diff tab input with the given URIs. + * @param original The uri of the original text resource. + * @param modified The uri of the modified text resource. + */ + constructor(original: Uri, modified: Uri); + } + + /** + * The tab represents a custom editor. + */ + export class TabInputCustom { + /** + * The uri which the tab is representing. + */ + readonly uri: Uri; + /** + * The type of custom editor. + */ + readonly viewType: string; + /** + * Constructs a custom editor tab input + * @param uri The uri of the tab + * @param viewType The viewtpye of the custom editor + */ + constructor(uri: Uri, viewType: string); + } + + /** + * The tab represents a webview. + */ + export class TabInputWebview { + /** + * The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + readonly viewType: string; + /** + * Constructs a webview tab input with the given view type. + * @param viewType The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + constructor(viewType: string); + } + + /** + * The tab represents a notebook. + */ + export class TabInputNotebook { + /** + * The uri which the tab is representing. + */ + readonly uri: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a new tab input for a notebook. + * @param uri The uri of the notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(uri: Uri, notebookType: string); + } + + /** + * The tabs represents two notebooks in a diff configuration. + */ + export class TabInputNotebookDiff { + /** + * The uri of the original notebook. + */ + readonly original: Uri; + /** + * The uri of the modified notebook. + */ + readonly modified: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a notebook diff tab input. + * @param original The uri of the original unmodified notebook. + * @param modified The uri of the modified notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(original: Uri, modified: Uri, notebookType: string); + } + + /** + * The tab represents a terminal in the editor area. + */ + export class TabInputTerminal { + /** + * Constructs a terminal tab input. + */ + constructor(); + } + + /** + * Represents a tab within a {@link TabGroup group of tabs}. + * Tabs are merely the graphical representation within the editor area. + * A backing editor is not a guarantee. + */ + export interface Tab { + + /** + * The text displayed on the tab + */ + readonly label: string; + + /** + * The group which the tab belongs to + */ + readonly group: TabGroup; + + /** + * Defines the structure of the tab i.e. text, notebook, custom, etc. + * Resource and other useful properties are defined on the tab kind. + */ + readonly input: TabInputText | TabInputTextDiff | TabInputCustom | TabInputWebview | TabInputNotebook | TabInputNotebookDiff | TabInputTerminal | unknown; + + /** + * Whether or not the tab is currently active. + * This is dictated by being the selected tab in the group + */ + readonly isActive: boolean; + + /** + * Whether or not the dirty indicator is present on the tab + */ + readonly isDirty: boolean; + + /** + * Whether or not the tab is pinned (pin icon is present) + */ + readonly isPinned: boolean; + + /** + * Whether or not the tab is in preview mode. + */ + readonly isPreview: boolean; + } + + /** + * An event describing change to tabs. + */ + export interface TabChangeEvent { + /** + * The tabs that have been opened + */ + readonly opened: readonly Tab[]; + /** + * The tabs that have been closed + */ + readonly closed: readonly Tab[]; + /** + * Tabs that have changed, e.g have changed + * their {@link Tab.isActive active} state. + */ + readonly changed: readonly Tab[]; + } + + /** + * An event describing changes to tab groups. + */ + export interface TabGroupChangeEvent { + /** + * Tab groups that have been opened. + */ + readonly opened: readonly TabGroup[]; + /** + * Tab groups that have been closed. + */ + readonly closed: readonly TabGroup[]; + /** + * Tab groups that have changed, e.g have changed + * their {@link TabGroup.isActive active} state. + */ + readonly changed: readonly TabGroup[]; + } + + /** + * Represents a group of tabs. A tab group itself consists of multiple tabs. + */ + export interface TabGroup { + /** + * Whether or not the group is currently active. + * + * *Note* that only one tab group is active at a time, but that multiple tab + * groups can have an {@link TabGroup.aciveTab active tab}. + * + * @see {@link Tab.isActive} + */ + readonly isActive: boolean; + + /** + * The view column of the group + */ + readonly viewColumn: ViewColumn; + + /** + * The active {@link Tab tab} in the group. This is the tab whose contents are currently + * being rendered. + * + * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. + */ + readonly activeTab: Tab | undefined; + + /** + * The list of tabs contained within the group. + * This can be empty if the group has no tabs open. + */ + readonly tabs: readonly Tab[]; + } + + /** + * Represents the main editor area which consists of multple groups which contain tabs. + */ + export interface TabGroups { + /** + * All the groups within the group container + */ + readonly all: readonly TabGroup[]; + + /** + * The currently active group + */ + readonly activeTabGroup: TabGroup; + + /** + * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. + */ + readonly onDidChangeTabGroups: Event; + + /** + * An {@link Event event} which fires when {@link Tab tabs} have changed. + */ + readonly onDidChangeTabs: Event; + + /** + * Closes the tab. This makes the tab object invalid and the tab + * should no longer be used for further actions. + * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid + * + * @param tab The tab to close. + * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + * @returns A promise that resolves to `true` when all tabs have been closed + */ + close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; + + /** + * Closes the tab group. This makes the tab group object invalid and the tab group + * should no longer be used for furhter actions. + * @param tabGroup The tab group to close. + * @param preserveFocus When `true` focus will remain in its current position. + * @returns A promise that resolves to `true` when all tab groups have been closed + */ + close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; + } } /** diff --git a/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts b/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts index 53e2d1e2a09..95fb288dd24 100644 --- a/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts +++ b/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts @@ -7,13 +7,18 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/127473 + /** + * The state of a comment thread. + */ export enum CommentThreadState { Unresolved = 0, Resolved = 1 } - // TODO@API doc export interface CommentThread { + /** + * The optional state of a comment thread, which may affect how the comment is displayed. + */ state?: CommentThreadState; } } diff --git a/src/vscode-dts/vscode.proposed.extensionsAny.d.ts b/src/vscode-dts/vscode.proposed.extensionsAny.d.ts new file mode 100644 index 00000000000..378e324fa3f --- /dev/null +++ b/src/vscode-dts/vscode.proposed.extensionsAny.d.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/145307 + + export interface Extension { + + /** + * `true` when the extension is associated to another extension host. + * + * *Note* that an extension from another extension host cannot export + * API, e.g {@link Extension.exports its exports} are always `undefined`. + */ + readonly isFromDifferentExtensionHost: boolean; + } + + export namespace extensions { + + /** + * Get an extension by its full identifier in the form of: `publisher.name`. + * + * @param extensionId An extension identifier. + * @param includeDifferentExtensionHosts Include extensions from different extension host + * @return An extension or `undefined`. + */ + export function getExtension(extensionId: string, includeDifferentExtensionHosts: boolean): Extension | undefined; + + /** + * All extensions across all extension hosts. + * + * @see {@link Extension.isFromDifferentExtensionHost} + */ + export const allAcrossExtensionHosts: readonly Extension[]; + + } +} diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts index 200b7ebda04..ed4bfed744f 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsNew.d.ts @@ -18,28 +18,37 @@ declare module 'vscode' { * not cause a failure of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. - * @param provider A inline completion provider. + * @param provider An inline completion provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerInlineCompletionItemProviderNew(selector: DocumentSelector, provider: InlineCompletionItemProviderNew): Disposable; } - // TODO@API doc + /** + * The inline completion item provider interface defines the contract between extensions and + * the inline completion feature. + * + * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. + */ export interface InlineCompletionItemProviderNew { /** * Provides inline completion items for the given position and document. * If inline completions are enabled, this method will be called whenever the user stopped typing. * It will also be called when the user explicitly triggers inline completions or asks for the next or previous inline completion. - * Use `context.triggerKind` to distinguish between these scenarios. - */ + * `context.triggerKind` can be used to distinguish between these scenarios. + */ + // TODO@API clarify "or asks for the next or previous inline completion"? Why would I return N items in the first place? + // TODO@API jsdoc for args, return-type provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContextNew, token: CancellationToken): ProviderResult; } - // TODO@API doc + /** + * Provides information about the context in which an inline completion was requested. + */ export interface InlineCompletionContextNew { /** - * How the completion was triggered. + * Describes how the inline completion was triggered. */ readonly triggerKind: InlineCompletionTriggerKindNew; @@ -52,21 +61,27 @@ declare module 'vscode' { * the inline completion must also replace `.` and start with `.log`, for example `.log()`. * * Inline completion providers are requested again whenever the selected item changes. - * - * The user must configure `"editor.suggest.preview": true` for this feature. - */ + */ readonly selectedCompletionInfo: SelectedCompletionInfoNew | undefined; } - // TODO@API find a better name, xyzFilter, xyzConstraint - // TODO@API doc + /** + * Describes the currently selected completion item. + */ export interface SelectedCompletionInfoNew { - range: Range; - text: string; + /** + * The range that will be replaced if this completion item is accepted. + */ + readonly range: Range; + + /** + * The text the range will be replaced with if this completion is accepted. + */ + readonly text: string; } /** - * How an {@link InlineCompletionItemProvider inline completion provider} was triggered. + * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. */ export enum InlineCompletionTriggerKindNew { /** @@ -82,38 +97,43 @@ declare module 'vscode' { Automatic = 1, } - // TODO@API doc + /** + * Represents a collection of {@link InlineCompletionItemNew inline completion items} to be presented + * in the editor. + */ + // TODO@API let keep this in `Additions` because of the insecurity about commands vs description export class InlineCompletionListNew { + /** + * The inline completion items. + */ items: InlineCompletionItemNew[]; - // TODO@API We could keep this and allow for `vscode.Command` instances that explain - // the result. That would replace the existing proposed menu-identifier and be more LSP friendly - // TODO@API maybe use MarkdownString - // commands?: Command[]; // "Show More..." - // description: MarkdownString - /** - * @deprecated Return an array of Inline Completion items directly. Will be removed eventually. - */ - constructor(items: InlineCompletionItemNew[]); + * A list of commands associated with the inline completions of this list. + */ + commands?: Command[]; + + // TODO@API jsdocs + constructor(items: InlineCompletionItemNew[], commands?: Command[]); } + /** + * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. + * + * @see {@link InlineCompletionItemProviderNew.provideInlineCompletionItems} + */ export class InlineCompletionItemNew { /** * The text to replace the range with. Must be set. * Is used both for the preview and the accept operation. - * - * The text the range refers to must be a subword of this value (`AB` and `BEF` are subwords of `ABCDEF`, but `Ab` is not). - * Additionally, if possible, it should be a prefix of this value for a better user-experience. - * - * However, any indentation of the text to replace does not matter for the subword constraint. - * Thus, ` B` can be replaced with ` ABC`, effectively removing a whitespace and inserting `A` and `C`. - */ + */ insertText: string | SnippetString; /** - * A text that is used to decide if this inline completion should be shown. - * An inline completion is shown if the text to replace is a subword of the filter text. + * A text that is used to decide if this inline completion should be shown. When `falsy` + * the {@link InlineCompletionItemNew.insertText} is used. + * + * An inline completion is shown if the text to replace is a prefix of the filter text. */ filterText?: string; @@ -121,11 +141,12 @@ declare module 'vscode' { * The range to replace. * Must begin and end on the same line. * + * TODO@API caching is an imlementation detail. drop that explanation? * Prefer replacements over insertions to avoid cache invalidation: * Instead of reporting a completion that inserts an extension at the end of a word, * the whole word (or even the whole line) should be replaced with the extended word (or extended line) to improve the UX. * That way, when the user presses backspace, the cache can be reused and there is no flickering. - */ + */ range?: Range; /** @@ -133,7 +154,13 @@ declare module 'vscode' { */ command?: Command; - // TODO@API doc + /** + * Creates a new inline completion item. + * + * @param insertText The text to replace the range with. + * @param range The range to replace. If not set, the word at the requested position will be used. + * @param command An optional {@link Command} that is executed *after* inserting this completion. + */ constructor(insertText: string | SnippetString, range?: Range, command?: Command); } } diff --git a/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts b/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts index ceb932ecdc6..66b2da1b68b 100644 --- a/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts +++ b/src/vscode-dts/vscode.proposed.inputBoxSeverity.d.ts @@ -7,25 +7,44 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/144944 + /** + * Impacts the behavior and appearance of the validation message. + */ export enum InputBoxValidationSeverity { Info = 1, Warning = 2, Error = 3 } + /** + * Object to configure the behavior of the validation message. + */ + export interface InputBoxValidationMessage { + /** + * The validation message to display. + */ + readonly message: string; + + /** + * The severity of the validation message. + * NOTE: When using `InputBoxValidationSeverity.Error`, the user will not be allowed to accept (hit ENTER) the input. + * `Info` and `Warning` will still allow the InputBox to accept the input. + */ + readonly severity: InputBoxValidationSeverity; + } + export interface InputBoxOptions { /** * The validation message to display. This will become the new {@link InputBoxOptions#validateInput} upon finalization. */ - // TODO@API consider to extract InputBoxValidationMessage - validateInput2?(value: string): string | { content: string; severity: InputBoxValidationSeverity } | undefined | null | - Thenable; + validateInput2?(value: string): string | InputBoxValidationMessage | undefined | null | + Thenable; } export interface InputBox { /** * The validation message to display. This will become the new {@link InputBox#validationMessage} upon finalization. */ - validationMessage2: string | { content: string; severity: InputBoxValidationSeverity } | undefined; + validationMessage2: string | InputBoxValidationMessage | undefined; } } diff --git a/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts b/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts deleted file mode 100644 index 259708cbe8e..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookConcatTextDocument.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/microsoft/vscode/issues/106744 - - export namespace notebooks { - /** - * @deprecated - */ - // todo@API really needed? we didn't find a user here - export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; - } - - /** @deprecated */ - export interface NotebookConcatTextDocument { - /** @deprecated */ - readonly uri: Uri; - /** @deprecated */ - readonly isClosed: boolean; - /** @deprecated */ - dispose(): void; - /** @deprecated */ - readonly onDidChange: Event; - /** @deprecated */ - readonly version: number; - /** @deprecated */ - getText(): string; - /** @deprecated */ - getText(range: Range): string; - - offsetAt(position: Position): number; - /** @deprecated */ - positionAt(offset: number): Position; - /** @deprecated */ - validateRange(range: Range): Range; - /** @deprecated */ - validatePosition(position: Position): Position; - - /** @deprecated */ - locationAt(positionOrRange: Position | Range): Location; - /** @deprecated */ - positionAt(location: Location): Position; - /** @deprecated */ - contains(uri: Uri): boolean; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts b/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts deleted file mode 100644 index f02680f147d..00000000000 --- a/src/vscode-dts/vscode.proposed.notebookDocumentEvents.d.ts +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// https://github.com/microsoft/vscode/issues/144662 - -declare module 'vscode' { - - /** - * Describes a change to a notebook cell. - * - * @see {@link NotebookDocumentChangeEvent} - */ - export interface NotebookDocumentContentCellChange { - - /** - * The affected notebook. - */ - readonly cell: NotebookCell; - - /** - * The document of the cell or `undefined` when it did not change. - * - * *Note* that you should use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event - * for detailed change information, like what edits have been performed. - */ - readonly document: TextDocument | undefined; - - /** - * The new metadata of the cell or `undefined` when it did not change. - */ - readonly metadata: { [key: string]: any } | undefined; - - /** - * The new outputs of the cell or `undefined` when they did not change. - */ - readonly outputs: readonly NotebookCellOutput[] | undefined; - - /** - * The new execution summary of the cell or `undefined` when it did not change. - */ - readonly executionSummary: NotebookCellExecutionSummary | undefined; - } - - /** - * Describes a structural change to a notebook document. - * - * @see {@link NotebookDocumentChangeEvent} - */ - export interface NotebookDocumentContentChange { - - /** - * The range at which cells have been either added or removed. - */ - readonly range: NotebookRange; - - /** - * Cells that have been added to the document. - */ - readonly addedCells: readonly NotebookCell[]; - - /** - * Cells that have been removed from the document. - */ - readonly removedCells: readonly NotebookCell[]; - } - - /** - * An event describing a transactional {@link NotebookDocument notebook} change. - */ - export interface NotebookDocumentChangeEvent { - - /** - * The affected notebook. - */ - readonly notebook: NotebookDocument; - - /** - * The new metadata of the notebook or `undefined` when it did not change. - */ - readonly metadata: { [key: string]: any } | undefined; - - /** - * An array of content changes describing added or removed {@link NotebookCell cells}. - */ - readonly contentChanges: readonly NotebookDocumentContentChange[]; - - /** - * An array of {@link NotebookDocumentContentCellChange cell changes}. - */ - readonly cellChanges: readonly NotebookDocumentContentCellChange[]; - } - - export namespace workspace { - - /** - * An event that is emitted when a {@link NotebookDocument notebook} is saved. - */ - export const onDidSaveNotebookDocument: Event; - - /** - * An event that is emitted when a {@link NotebookDocument notebook} has changed. - */ - export const onDidChangeNotebookDocument: Event; - } -} diff --git a/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts b/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts new file mode 100644 index 00000000000..07f8e833f15 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.notebookProxyController.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + + export interface NotebookProxyController { + /** + * The identifier of this notebook controller. + * + * _Note_ that controllers are remembered by their identifier and that extensions should use + * stable identifiers across sessions. + */ + readonly id: string; + + /** + * The notebook type this controller is for. + */ + readonly notebookType: string; + + /** + * The human-readable label of this notebook controller. + */ + label: string; + + /** + * The human-readable description which is rendered less prominent. + */ + description?: string; + + /** + * The human-readable detail which is rendered less prominent. + */ + detail?: string; + + /** + * The human-readable label used to categorise controllers. + */ + kind?: string; + + resolveHandler: () => NotebookController | string | Thenable; + + readonly onDidChangeSelectedNotebooks: Event<{ readonly notebook: NotebookDocument; readonly selected: boolean }>; + + /** + * Dispose and free associated resources. + */ + dispose(): void; + } + + export namespace notebooks { + export function createNotebookProxyController(id: string, notebookType: string, label: string, resolveHandler: () => NotebookController | string | Thenable): NotebookProxyController; + } +} diff --git a/src/vscode-dts/vscode.proposed.tabs.d.ts b/src/vscode-dts/vscode.proposed.tabs.d.ts deleted file mode 100644 index 273441b617e..00000000000 --- a/src/vscode-dts/vscode.proposed.tabs.d.ts +++ /dev/null @@ -1,252 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode' { - - // https://github.com/Microsoft/vscode/issues/15178 - - // TODO@API name alternatives for TabKind: TabInput, TabOptions, - - - /** - * The tab represents a single text based resource - */ - export class TabKindText { - /** - * The uri represented by the tab. - */ - readonly uri: Uri; - constructor(uri: Uri); - } - - /** - * The tab represents two text based resources - * being rendered as a diff. - */ - export class TabKindTextDiff { - /** - * The uri of the original text resource. - */ - readonly original: Uri; - /** - * The uri of the modified text resource. - */ - readonly modified: Uri; - constructor(original: Uri, modified: Uri); - } - - /** - * The tab represents a custom editor. - */ - export class TabKindCustom { - /** - * The uri which the tab is representing. - */ - readonly uri: Uri; - /** - * The type of custom editor. - */ - readonly viewType: string; - constructor(uri: Uri, viewType: string); - } - - /** - * The tab represents a webview. - */ - export class TabKindWebview { - /** - * The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} - */ - readonly viewType: string; - constructor(viewType: string); - } - - /** - * The tab represents a notebook. - */ - export class TabKindNotebook { - /** - * The uri which the tab is representing. - */ - readonly uri: Uri; - /** - * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} - */ - readonly notebookType: string; - constructor(uri: Uri, notebookType: string); - } - - /** - * The tabs represents two notebooks in a diff configuration. - */ - export class TabKindNotebookDiff { - /** - * The uri of the original notebook. - */ - readonly original: Uri; - /** - * The uri of the modified notebook. - */ - readonly modified: Uri; - readonly notebookType: string; - constructor(original: Uri, modified: Uri, notebookType: string); - } - - /** - * The tab represents a terminal in the editor area. - */ - export class TabKindTerminal { - constructor(); - } - - /** - * Represents a tab within a {@link TabGroup group of tabs}. - * Tabs are merely the graphical representation within the editor area. - * A backing editor is not a guarantee. - */ - export interface Tab { - - /** - * The text displayed on the tab - */ - readonly label: string; - - /** - * The group which the tab belongs to - */ - readonly group: TabGroup; - - /** - * Defines the structure of the tab i.e. text, notebook, custom, etc. - * Resource and other useful properties are defined on the tab kind. - */ - readonly kind: TabKindText | TabKindTextDiff | TabKindCustom | TabKindWebview | TabKindNotebook | TabKindNotebookDiff | TabKindTerminal | unknown; - - /** - * Whether or not the tab is currently active. - * This is dictated by being the selected tab in the group - */ - readonly isActive: boolean; - - /** - * Whether or not the dirty indicator is present on the tab - */ - readonly isDirty: boolean; - - /** - * Whether or not the tab is pinned (pin icon is present) - */ - readonly isPinned: boolean; - - /** - * Whether or not the tab is in preview mode. - */ - readonly isPreview: boolean; - } - - export namespace window { - /** - * Represents the grid widget within the main editor area - */ - export const tabGroups: TabGroups; - } - - export interface TabChangeEvent { - // TODO@API consider: opened - readonly added: readonly Tab[]; - // TODO@API consider: closed (aligns with TabGroups.close(...)) - readonly removed: readonly Tab[]; - readonly changed: readonly Tab[]; - } - - /** - * Represents a group of tabs. A tab group itself consists of multiple tab - */ - export interface TabGroup { - /** - * Whether or not the group is currently active. - * - * *Note* that only one tab group is active at a time, but that multiple tab - * groups can have an {@link TabGroup.aciveTab active tab}. - * - * @see {@link Tab.isActive} - */ - readonly isActive: boolean; - - /** - * The view column of the group - */ - readonly viewColumn: ViewColumn; - - /** - * The active {@link Tab tab} in the group. This is the tab which contents are currently - * being rendered. - * - * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. - */ - readonly activeTab: Tab | undefined; - - /** - * The list of tabs contained within the group. - * This can be empty if the group has no tabs open. - */ - readonly tabs: readonly Tab[]; - } - - export interface TabGroups { - /** - * All the groups within the group container - */ - readonly all: readonly TabGroup[]; - - /** - * The currently active group - */ - readonly activeTabGroup: TabGroup; - - /** - * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. - */ - // TODO@API consider `TabGroupChangeEvent` similar to `TabChangeEvent` - readonly onDidChangeTabGroups: Event; - - /** - * An {@link Event event} which fires when {@link Tab tabs} have changed. - */ - readonly onDidChangeTabs: Event; - - /** - * Closes the tab. This makes the tab object invalid and the tab - * should no longer be used for further actions. - * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid - * - * @param tab The tab to close. - * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. - * @returns A promise that resolves to `true` when all tabs have been closed - */ - close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; - - /** - * Closes the tab group. This makes the tab group object invalid and the tab group - * should no longer be used for furhter actions. - * @param tabGroup The tab group to close. - * @param preserveFocus When `true` focus will remain in its current position. - * @returns A promise that resolves to `true` when all tab groups have been closed - */ - close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; - - /** - * Moves a tab to the given index within the column. - * If the index is out of range, the tab will be moved to the end of the column. - * If the column is out of range, a new one will be created after the last existing column. - * - * @package tab The tab to move. - * @param viewColumn The column to move the tab into - * @param index The index to move the tab to - */ - // TODO@API remove for now - move(tab: Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean): Thenable; - } -} diff --git a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts index f0ea3649a82..879e5a757b7 100644 --- a/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts +++ b/src/vscode-dts/vscode.proposed.textEditorDrop.d.ts @@ -16,7 +16,7 @@ declare module 'vscode' { /** * Provider which handles dropping of resources into a text editor. * - * The user can drop into a text editor by holding down `shift` while dragging. Requires `workbench.editor.dropIntoEditor.enabled` to be on. + * The user can drop into a text editor by holding down `shift` while dragging. Requires `workbench.experimental.editor.dropIntoEditor.enabled` to be on. */ export interface DocumentOnDropProvider { /** diff --git a/test/automation/package.json b/test/automation/package.json index 7a7b3c5a560..96397298161 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -9,13 +9,11 @@ "main": "./out/index.js", "private": true, "scripts": { - "compile": "npm run copy-driver && npm run copy-driver-definition && node ../../node_modules/typescript/bin/tsc", - "watch": "npm-run-all -lp watch-driver watch-driver-definition watch-tsc", + "compile": "npm run copy-driver-definition && node ../../node_modules/typescript/bin/tsc", + "watch": "npm-run-all -lp watch-driver-definition watch-tsc", "watch-tsc": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput", - "copy-driver": "cpx src/driver.js out/", - "watch-driver": "cpx src/driver.js out/ -w", "copy-driver-definition": "node tools/copy-driver-definition.js", - "watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/node", + "watch-driver-definition": "watch \"node tools/copy-driver-definition.js\"", "copy-package-version": "node tools/copy-package-version.js", "prepublishOnly": "npm run copy-package-version" }, diff --git a/test/automation/src/application.ts b/test/automation/src/application.ts index 23c9e630ea2..c65f1cfd701 100644 --- a/test/automation/src/application.ts +++ b/test/automation/src/application.ts @@ -6,7 +6,6 @@ import { Workbench } from './workbench'; import { Code, launch, LaunchOptions } from './code'; import { Logger, measureAndLog } from './logger'; -import { PlaywrightDriver } from './playwrightDriver'; export const enum Quality { Dev, @@ -16,8 +15,7 @@ export const enum Quality { export interface ApplicationOptions extends LaunchOptions { quality: Quality; - workspacePath: string; - waitTime: number; + readonly workspacePath: string; } export class Application { @@ -49,10 +47,6 @@ export class Application { return !!this.options.web; } - get legacy(): boolean { - return !!this.options.legacy; - } - private _workspacePathOrFolder: string; get workspacePathOrFolder(): string { return this._workspacePathOrFolder; @@ -73,8 +67,10 @@ export class Application { } async restart(options?: { workspaceOrFolder?: string; extraArgs?: string[] }): Promise { - await this.stop(); - await this._start(options?.workspaceOrFolder, options?.extraArgs); + await measureAndLog((async () => { + await this.stop(); + await this._start(options?.workspaceOrFolder, options?.extraArgs); + })(), 'Application#restart()', this.logger); } private async _start(workspaceOrFolder = this.workspacePathOrFolder, extraArgs: string[] = []): Promise { @@ -118,11 +114,8 @@ export class Application { private async checkWindowReady(code: Code): Promise { - // This is legacy and will be removed when our old driver removes - await code.waitForWindowIds(ids => ids.length > 0); - // We need a rendered workbench - await this.checkWorkbenchReady(code); + await measureAndLog(code.waitForElement('.monaco-workbench'), 'Application#checkWindowReady: wait for .monaco-workbench element', this.logger); // Remote but not web: wait for a remote connection state change if (this.remote) { @@ -141,28 +134,4 @@ export class Application { }, 300 /* = 30s of retry */), 'Application#checkWindowReady: wait for remote indicator', this.logger); } } - - private async checkWorkbenchReady(code: Code): Promise { - const driver = code.driver; - - // Web / Legacy: just poll for workbench element - if (this.web || !(driver instanceof PlaywrightDriver)) { - await measureAndLog(code.waitForElement('.monaco-workbench'), 'Application#checkWindowReady: wait for .monaco-workbench element', this.logger); - } - - // Desktop (playwright): we see hangs, where IPC messages - // are not delivered (https://github.com/microsoft/vscode/issues/146785) - // Workaround is to try to reload the window when that happens - else { - try { - await measureAndLog(code.waitForElement('.monaco-workbench', undefined, 100 /* 10s of retry */), 'Application#checkWindowReady: wait for .monaco-workbench element', this.logger); - } catch (error) { - this.logger.log(`checkWindowReady: giving up after 10s, reloading window and trying again...`); - - await driver.reload(); - - return this.checkWorkbenchReady(code); - } - } - } } diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 4ffdd0ae203..5e6b52c7d73 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -6,13 +6,14 @@ import { join } from 'path'; import * as os from 'os'; import * as cp from 'child_process'; -import { IDriver, IDisposable, IElement, Thenable, ILocalizedStrings, ILocaleInfo } from './driver'; -import { launch as launchElectron } from './electron'; +import { IElement, ILocalizedStrings, ILocaleInfo } from './driver'; import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { Logger, measureAndLog } from './logger'; import { copyExtension } from './extensions'; import * as treekill from 'tree-kill'; +import { teardown } from './processes'; +import { PlaywrightDriver } from './playwrightDriver'; const rootPath = join(__dirname, '../../..'); @@ -27,7 +28,6 @@ export interface LaunchOptions { readonly extraArgs?: string[]; readonly remote?: boolean; readonly web?: boolean; - readonly legacy?: boolean; readonly tracing?: boolean; readonly headless?: boolean; readonly browser?: 'chromium' | 'webkit' | 'firefox'; @@ -39,8 +39,8 @@ interface ICodeInstance { const instances = new Set(); -function registerInstance(process: cp.ChildProcess, logger: Logger, type: string, kill: () => Promise) { - const instance = { kill }; +function registerInstance(process: cp.ChildProcess, logger: Logger, type: string) { + const instance = { kill: () => teardown(process, logger) }; instances.add(instance); process.stdout?.on('data', data => logger.log(`[${type}] stdout: ${data}`)); @@ -53,7 +53,7 @@ function registerInstance(process: cp.ChildProcess, logger: Logger, type: string }); } -async function teardown(signal?: number) { +async function teardownAll(signal?: number) { stopped = true; for (const instance of instances) { @@ -66,9 +66,9 @@ async function teardown(signal?: number) { } let stopped = false; -process.on('exit', () => teardown()); -process.on('SIGINT', () => teardown(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events -process.on('SIGTERM', () => teardown(128 + 15)); // same as above +process.on('exit', () => teardownAll()); +process.on('SIGINT', () => teardownAll(128 + 2)); // https://nodejs.org/docs/v14.16.0/api/process.html#process_signal_events +process.on('SIGTERM', () => teardownAll(128 + 15)); // same as above export async function launch(options: LaunchOptions): Promise { if (stopped) { @@ -79,37 +79,29 @@ export async function launch(options: LaunchOptions): Promise { // Browser smoke tests if (options.web) { - const { serverProcess, client, driver, kill } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); - registerInstance(serverProcess, options.logger, 'server', kill); + const { serverProcess, driver } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); + registerInstance(serverProcess, options.logger, 'server'); - return new Code(client, driver, options.logger); + return new Code(driver, options.logger, serverProcess); } // Electron smoke tests (playwright) - else if (!options.legacy) { - const { client, driver } = await measureAndLog(launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); - - return new Code(client, driver, options.logger); - } - - // Electron smoke tests (legacy driver) else { - const { electronProcess, client, driver, kill } = await measureAndLog(launchElectron(options), 'launch electron', options.logger); - registerInstance(electronProcess, options.logger, 'electron', kill); + const { electronProcess, driver } = await measureAndLog(launchPlaywrightElectron(options), 'launch playwright (electron)', options.logger); + registerInstance(electronProcess, options.logger, 'electron'); - return new Code(client, driver, options.logger); + return new Code(driver, options.logger, electronProcess); } } export class Code { - private _activeWindowId: number | undefined = undefined; - readonly driver: IDriver; + readonly driver: PlaywrightDriver; constructor( - private client: IDisposable, - driver: IDriver, - readonly logger: Logger + driver: PlaywrightDriver, + readonly logger: Logger, + private readonly mainProcess: cp.ChildProcess ) { this.driver = new Proxy(driver, { get(target, prop) { @@ -131,32 +123,27 @@ export class Code { } async startTracing(name: string): Promise { - const windowId = await this.getActiveWindowId(); - return await this.driver.startTracing(windowId, name); + return await this.driver.startTracing(name); } async stopTracing(name: string, persist: boolean): Promise { - const windowId = await this.getActiveWindowId(); - return await this.driver.stopTracing(windowId, name, persist); - } - - async waitForWindowIds(accept: (windowIds: number[]) => boolean): Promise { - await this.poll(() => this.driver.getWindowIds(), accept, `get window ids`); + return await this.driver.stopTracing(name, persist); } async dispatchKeybinding(keybinding: string): Promise { - const windowId = await this.getActiveWindowId(); - await this.driver.dispatchKeybinding(windowId, keybinding); + await this.driver.dispatchKeybinding(keybinding); } async exit(): Promise { - - // Start the exit flow via driver - const pid = await measureAndLog(this.driver.exitApplication(), 'driver.exitApplication()', this.logger); - return measureAndLog(new Promise((resolve, reject) => { + const pid = this.mainProcess.pid!; + let done = false; + // Start the exit flow via driver + this.driver.exitApplication(); + + // Await the exit of the application (async () => { let retries = 0; while (!done) { @@ -190,17 +177,14 @@ export class Code { } } })(); - }).finally(() => { - this.dispose(); }), 'Code#exit()', this.logger); } async waitForTextContent(selector: string, textContent?: string, accept?: (result: string) => boolean, retryCount?: number): Promise { - const windowId = await this.getActiveWindowId(); accept = accept || (result => textContent !== undefined ? textContent === result : !!result); return await this.poll( - () => this.driver.getElements(windowId, selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))), + () => this.driver.getElements(selector).then(els => els.length > 0 ? Promise.resolve(els[0].textContent) : Promise.reject(new Error('Element not found for textContent'))), s => accept!(typeof s === 'string' ? s : ''), `get text content '${selector}'`, retryCount @@ -208,77 +192,51 @@ export class Code { } async waitAndClick(selector: string, xoffset?: number, yoffset?: number, retryCount: number = 200): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.click(windowId, selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount); + await this.poll(() => this.driver.click(selector, xoffset, yoffset), () => true, `click '${selector}'`, retryCount); } async waitForSetValue(selector: string, value: string): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.setValue(windowId, selector, value), () => true, `set value '${selector}'`); + await this.poll(() => this.driver.setValue(selector, value), () => true, `set value '${selector}'`); } async waitForElements(selector: string, recursive: boolean, accept: (result: IElement[]) => boolean = result => result.length > 0): Promise { - const windowId = await this.getActiveWindowId(); - return await this.poll(() => this.driver.getElements(windowId, selector, recursive), accept, `get elements '${selector}'`); + return await this.poll(() => this.driver.getElements(selector, recursive), accept, `get elements '${selector}'`); } async waitForElement(selector: string, accept: (result: IElement | undefined) => boolean = result => !!result, retryCount: number = 200): Promise { - const windowId = await this.getActiveWindowId(); - return await this.poll(() => this.driver.getElements(windowId, selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount); + return await this.poll(() => this.driver.getElements(selector).then(els => els[0]), accept, `get element '${selector}'`, retryCount); } async waitForActiveElement(selector: string, retryCount: number = 200): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.isActiveElement(windowId, selector), r => r, `is active element '${selector}'`, retryCount); + await this.poll(() => this.driver.isActiveElement(selector), r => r, `is active element '${selector}'`, retryCount); } async waitForTitle(accept: (title: string) => boolean): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.getTitle(windowId), accept, `get title`); + await this.poll(() => this.driver.getTitle(), accept, `get title`); } async waitForTypeInEditor(selector: string, text: string): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.typeInEditor(windowId, selector, text), () => true, `type in editor '${selector}'`); + await this.poll(() => this.driver.typeInEditor(selector, text), () => true, `type in editor '${selector}'`); } async waitForTerminalBuffer(selector: string, accept: (result: string[]) => boolean): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.getTerminalBuffer(windowId, selector), accept, `get terminal buffer '${selector}'`); + await this.poll(() => this.driver.getTerminalBuffer(selector), accept, `get terminal buffer '${selector}'`); } async writeInTerminal(selector: string, value: string): Promise { - const windowId = await this.getActiveWindowId(); - await this.poll(() => this.driver.writeInTerminal(windowId, selector, value), () => true, `writeInTerminal '${selector}'`); + await this.poll(() => this.driver.writeInTerminal(selector, value), () => true, `writeInTerminal '${selector}'`); } async getLocaleInfo(): Promise { - const windowId = await this.getActiveWindowId(); - return this.driver.getLocaleInfo(windowId); + return this.driver.getLocaleInfo(); } async getLocalizedStrings(): Promise { - const windowId = await this.getActiveWindowId(); - return this.driver.getLocalizedStrings(windowId); - } - - private async getActiveWindowId(): Promise { - if (typeof this._activeWindowId !== 'number') { - this.logger.log('getActiveWindowId(): begin'); - const windows = await this.driver.getWindowIds(); - this._activeWindowId = windows[0]; - this.logger.log(`getActiveWindowId(): end (windowId=${this._activeWindowId})`); - } - - return this._activeWindowId; - } - - dispose(): void { - this.client.dispose(); + return this.driver.getLocalizedStrings(); } private async poll( - fn: () => Thenable, + fn: () => Promise, acceptFn: (result: T) => boolean, timeoutMessage: string, retryCount = 200, diff --git a/test/automation/src/debug.ts b/test/automation/src/debug.ts index eedc400451f..b7b7d427f4b 100644 --- a/test/automation/src/debug.ts +++ b/test/automation/src/debug.ts @@ -8,7 +8,7 @@ import { Commands } from './workbench'; import { Code, findElement } from './code'; import { Editors } from './editors'; import { Editor } from './editor'; -import { IElement } from '../src/driver'; +import { IElement } from './driver'; const VIEWLET = 'div[id="workbench.view.debug"]'; const DEBUG_VIEW = `${VIEWLET}`; diff --git a/test/automation/src/driver.js b/test/automation/src/driver.js deleted file mode 100644 index c415029cdf9..00000000000 --- a/test/automation/src/driver.js +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check -'use strict'; - -const path = require('path'); - -exports.connect = function (outPath, handle) { - const bootstrapPath = path.join(outPath, 'bootstrap-amd.js'); - const { load } = require(bootstrapPath); - - return new Promise((resolve, reject) => load('vs/platform/driver/node/driver', ({ connect }) => connect(handle).then(resolve, reject), reject)); -}; diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index 8f9f8dcd8f9..42e46a60a0b 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -4,16 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { join } from 'path'; -import { platform } from 'os'; -import { tmpName } from 'tmp'; -import { connect as connectElectronDriver, IDisposable, IDriver } from './driver'; -import { ChildProcess, spawn, SpawnOptions } from 'child_process'; import * as mkdirp from 'mkdirp'; -import { promisify } from 'util'; -import * as kill from 'tree-kill'; import { copyExtension } from './extensions'; import { URI } from 'vscode-uri'; -import { Logger, measureAndLog } from './logger'; +import { measureAndLog } from './logger'; import type { LaunchOptions } from './code'; const root = join(__dirname, '..', '..', '..'); @@ -96,87 +90,6 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom }; } -/** - * @deprecated should use the playwright based electron support instead - */ -export async function launch(options: LaunchOptions): Promise<{ electronProcess: ChildProcess; client: IDisposable; driver: IDriver; kill: () => Promise }> { - const { codePath, logger, verbose } = options; - const { env, args, electronPath } = await resolveElectronConfiguration(options); - - const driverIPCHandle = await measureAndLog(createDriverHandle(), 'createDriverHandle', logger); - args.push('--driver', driverIPCHandle); - - const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); - - const spawnOptions: SpawnOptions = { env }; - - if (verbose) { - spawnOptions.stdio = ['ignore', 'inherit', 'inherit']; - } - - const electronProcess = spawn(electronPath, args, spawnOptions); - - logger.log(`Started electron for desktop smoke tests on pid ${electronProcess.pid}`); - - let retries = 0; - - while (true) { - try { - const { client, driver } = await measureAndLog(connectElectronDriver(outPath, driverIPCHandle), 'connectElectronDriver()', logger); - return { - electronProcess, - client, - driver, - kill: () => teardown(electronProcess, options.logger) - }; - } catch (err) { - - // give up - if (++retries > 30) { - logger.log(`Error connecting driver: ${err}. Giving up...`); - - await measureAndLog(teardown(electronProcess, logger), 'Kill Electron after failing to connect', logger); - - throw err; - } - - // retry - else { - if ((err as NodeJS.ErrnoException).code !== 'ENOENT' /* ENOENT is expected for as long as the server has not started on the socket */) { - logger.log(`Error connecting driver: ${err}. Attempting to retry...`); - } - - await new Promise(resolve => setTimeout(resolve, 1000)); - } - } - } -} - -async function teardown(electronProcess: ChildProcess, logger: Logger): Promise { - const electronPid = electronProcess.pid; - if (typeof electronPid !== 'number') { - return; - } - - let retries = 0; - while (retries < 3) { - retries++; - - try { - return await promisify(kill)(electronPid); - } catch (error) { - try { - process.kill(electronPid, 0); // throws an exception if the process doesn't exist anymore - logger.log(`Error tearing down electron client (pid: ${electronPid}, attempt: ${retries}): ${error}`); - } catch (error) { - return; // Expected when process is gone - } - } - } - - logger.log(`Gave up tearing down electron client after ${retries} attempts...`); -} - export function getDevElectronPath(): string { const buildPath = join(root, '.build'); const product = require(join(root, 'product.json')); @@ -218,28 +131,3 @@ export function getBuildVersion(root: string): string { return require(join(root, 'resources', 'app', 'package.json')).version; } } - -function getDevOutPath(): string { - return join(root, 'out'); -} - -function getBuildOutPath(root: string): string { - switch (process.platform) { - case 'darwin': - return join(root, 'Contents', 'Resources', 'app', 'out'); - default: - return join(root, 'resources', 'app', 'out'); - } -} - -async function createDriverHandle(): Promise { - - // Windows - if ('win32' === platform()) { - const name = [...Array(15)].map(() => Math.random().toString(36)[3]).join(''); - return `\\\\.\\pipe\\${name}`; - } - - // Posix - return promisify(tmpName)(); -} diff --git a/test/automation/src/index.ts b/test/automation/src/index.ts index e5ffa4e60cb..ba417bf1f27 100644 --- a/test/automation/src/index.ts +++ b/test/automation/src/index.ts @@ -25,5 +25,4 @@ export * from './terminal'; export * from './viewlet'; export * from './localization'; export * from './workbench'; -export * from './driver'; export { getDevElectronPath, getBuildElectronPath, getBuildVersion } from './electron'; diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index a2851762385..f4e28668ab0 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -6,11 +6,8 @@ import * as playwright from '@playwright/test'; import { ChildProcess, spawn } from 'child_process'; import { join } from 'path'; -import { mkdir } from 'fs'; -import { promisify } from 'util'; -import { IDriver, IDisposable } from './driver'; +import * as mkdirp from 'mkdirp'; import { URI } from 'vscode-uri'; -import * as kill from 'tree-kill'; import { Logger, measureAndLog } from './logger'; import type { LaunchOptions } from './code'; import { PlaywrightDriver } from './playwrightDriver'; @@ -19,7 +16,7 @@ const root = join(__dirname, '..', '..', '..'); let port = 9000; -export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; client: IDisposable; driver: IDriver; kill: () => Promise }> { +export async function launch(options: LaunchOptions): Promise<{ serverProcess: ChildProcess; driver: PlaywrightDriver }> { // Launch server const { serverProcess, endpoint } = await launchServer(options); @@ -29,11 +26,7 @@ export async function launch(options: LaunchOptions): Promise<{ serverProcess: C return { serverProcess, - client: { - dispose: () => { /* there is no client to dispose for browser, teardown is triggered via exitApplication call */ } - }, - driver: new PlaywrightDriver(browser, context, page, serverProcess.pid, options), - kill: () => teardown(serverProcess.pid, options.logger) + driver: new PlaywrightDriver(browser, context, page, serverProcess, options) }; } @@ -41,7 +34,7 @@ async function launchServer(options: LaunchOptions) { const { userDataDir, codePath, extensionsPath, logger, logsPath } = options; const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; const agentFolder = userDataDir; - await measureAndLog(promisify(mkdir)(agentFolder), `mkdir(${agentFolder})`, logger); + await measureAndLog(mkdirp(agentFolder), `mkdirp(${agentFolder})`, logger); const env = { VSCODE_REMOTE_SERVER_PATH: codeServerPath, @@ -145,30 +138,6 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) { return { browser, context, page }; } -export async function teardown(serverPid: number | undefined, logger: Logger): Promise { - if (typeof serverPid !== 'number') { - return; - } - - let retries = 0; - while (retries < 3) { - retries++; - - try { - return await promisify(kill)(serverPid); - } catch (error) { - try { - process.kill(serverPid, 0); // throws an exception if the process doesn't exist anymore - logger.log(`Error tearing down server (pid: ${serverPid}, attempt: ${retries}): ${error}`); - } catch (error) { - return; // Expected when process is gone - } - } - } - - logger.log(`Gave up tearing down server after ${retries} attempts...`); -} - function waitForEndpoint(server: ChildProcess, logger: Logger): Promise { return new Promise((resolve, reject) => { let endpointFound = false; diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 6a28ed401fa..61e219db4d5 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -5,13 +5,14 @@ import * as playwright from '@playwright/test'; import { join } from 'path'; -import { IDriver, IWindowDriver } from './driver'; +import { IWindowDriver } from './driver'; import { PageFunction } from 'playwright-core/types/structs'; import { measureAndLog } from './logger'; import { LaunchOptions } from './code'; -import { teardown } from './playwrightBrowser'; +import { teardown } from './processes'; +import { ChildProcess } from 'child_process'; -export class PlaywrightDriver implements IDriver { +export class PlaywrightDriver { private static traceCounter = 1; private static screenShotCounter = 1; @@ -30,22 +31,16 @@ export class PlaywrightDriver implements IDriver { esc: 'Escape' }; - _serviceBrand: undefined; - constructor( private readonly application: playwright.Browser | playwright.ElectronApplication, private readonly context: playwright.BrowserContext, private readonly page: playwright.Page, - private readonly serverPid: number | undefined, + private readonly serverProcess: ChildProcess | undefined, private readonly options: LaunchOptions ) { } - async getWindowIds() { - return [1]; - } - - async startTracing(windowId: number, name: string): Promise { + async startTracing(name: string): Promise { if (!this.options.tracing) { return; // tracing disabled } @@ -57,7 +52,7 @@ export class PlaywrightDriver implements IDriver { } } - async stopTracing(windowId: number, name: string, persist: boolean): Promise { + async stopTracing(name: string, persist: boolean): Promise { if (!this.options.tracing) { return; // tracing disabled } @@ -107,35 +102,31 @@ export class PlaywrightDriver implements IDriver { // Ignore } - // VSCode shutdown (desktop only) - let mainPid: number | undefined = undefined; - if (!this.options.web) { + // Web: exit via `close` method + if (this.options.web) { try { - mainPid = await measureAndLog(this._evaluateWithDriver(([driver]) => (driver as unknown as IDriver).exitApplication()), 'driver.exitApplication()', this.options.logger); + await measureAndLog(this.application.close(), 'playwright.close()', this.options.logger); + } catch (error) { + this.options.logger.log(`Error closing appliction (${error})`); + } + } + + // Desktop: exit via `driver.exitApplication` + else { + try { + await measureAndLog(this.evaluateWithDriver(([driver]) => driver.exitApplication()), 'driver.exitApplication()', this.options.logger); } catch (error) { this.options.logger.log(`Error exiting appliction (${error})`); } } - // Playwright shutdown - try { - await Promise.race([ - measureAndLog(this.application.close(), 'playwright.close()', this.options.logger), - new Promise(resolve => setTimeout(() => resolve(), 10000)) // TODO@bpasero mitigate https://github.com/microsoft/vscode/issues/146803 - ]); - } catch (error) { - this.options.logger.log(`Error closing appliction (${error})`); + // Server: via `teardown` + if (this.serverProcess) { + await measureAndLog(teardown(this.serverProcess, this.options.logger), 'teardown server process', this.options.logger); } - - // Server shutdown - if (typeof this.serverPid === 'number') { - await measureAndLog(teardown(this.serverPid, this.options.logger), 'teardown server', this.options.logger); - } - - return mainPid ?? this.serverPid! /* when running web we must have a server Pid */; } - async dispatchKeybinding(windowId: number, keybinding: string) { + async dispatchKeybinding(keybinding: string) { const chords = keybinding.split(' '); for (let i = 0; i < chords.length; i++) { const chord = chords[i]; @@ -165,60 +156,60 @@ export class PlaywrightDriver implements IDriver { await this.timeout(100); } - async click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined) { - const { x, y } = await this.getElementXY(windowId, selector, xoffset, yoffset); + async click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined) { + const { x, y } = await this.getElementXY(selector, xoffset, yoffset); await this.page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0)); } - async setValue(windowId: number, selector: string, text: string) { - return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this._getDriverHandle(), selector, text] as const); + async setValue(selector: string, text: string) { + return this.page.evaluate(([driver, selector, text]) => driver.setValue(selector, text), [await this.getDriverHandle(), selector, text] as const); } - async getTitle(windowId: number) { - return this._evaluateWithDriver(([driver]) => driver.getTitle()); + async getTitle() { + return this.evaluateWithDriver(([driver]) => driver.getTitle()); } - async isActiveElement(windowId: number, selector: string) { - return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this._getDriverHandle(), selector] as const); + async isActiveElement(selector: string) { + return this.page.evaluate(([driver, selector]) => driver.isActiveElement(selector), [await this.getDriverHandle(), selector] as const); } - async getElements(windowId: number, selector: string, recursive: boolean = false) { - return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this._getDriverHandle(), selector, recursive] as const); + async getElements(selector: string, recursive: boolean = false) { + return this.page.evaluate(([driver, selector, recursive]) => driver.getElements(selector, recursive), [await this.getDriverHandle(), selector, recursive] as const); } - async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number) { - return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this._getDriverHandle(), selector, xoffset, yoffset] as const); + async getElementXY(selector: string, xoffset?: number, yoffset?: number) { + return this.page.evaluate(([driver, selector, xoffset, yoffset]) => driver.getElementXY(selector, xoffset, yoffset), [await this.getDriverHandle(), selector, xoffset, yoffset] as const); } - async typeInEditor(windowId: number, selector: string, text: string) { - return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this._getDriverHandle(), selector, text] as const); + async typeInEditor(selector: string, text: string) { + return this.page.evaluate(([driver, selector, text]) => driver.typeInEditor(selector, text), [await this.getDriverHandle(), selector, text] as const); } - async getTerminalBuffer(windowId: number, selector: string) { - return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this._getDriverHandle(), selector] as const); + async getTerminalBuffer(selector: string) { + return this.page.evaluate(([driver, selector]) => driver.getTerminalBuffer(selector), [await this.getDriverHandle(), selector] as const); } - async writeInTerminal(windowId: number, selector: string, text: string) { - return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this._getDriverHandle(), selector, text] as const); + async writeInTerminal(selector: string, text: string) { + return this.page.evaluate(([driver, selector, text]) => driver.writeInTerminal(selector, text), [await this.getDriverHandle(), selector, text] as const); } - async getLocaleInfo(windowId: number) { - return this._evaluateWithDriver(([driver]) => driver.getLocaleInfo()); + async getLocaleInfo() { + return this.evaluateWithDriver(([driver]) => driver.getLocaleInfo()); } - async getLocalizedStrings(windowId: number) { - return this._evaluateWithDriver(([driver]) => driver.getLocalizedStrings()); + async getLocalizedStrings() { + return this.evaluateWithDriver(([driver]) => driver.getLocalizedStrings()); } - private async _evaluateWithDriver(pageFunction: PageFunction[], T>) { - return this.page.evaluate(pageFunction, [await this._getDriverHandle()]); + private async evaluateWithDriver(pageFunction: PageFunction[], T>) { + return this.page.evaluate(pageFunction, [await this.getDriverHandle()]); } private timeout(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } - private async _getDriverHandle(): Promise> { + private async getDriverHandle(): Promise> { return this.page.evaluateHandle('window.driver'); } } diff --git a/test/automation/src/playwrightElectron.ts b/test/automation/src/playwrightElectron.ts index 56677298146..3ad47fbb2ec 100644 --- a/test/automation/src/playwrightElectron.ts +++ b/test/automation/src/playwrightElectron.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as playwright from '@playwright/test'; -import { IDriver, IDisposable } from './driver'; import type { LaunchOptions } from './code'; import { PlaywrightDriver } from './playwrightDriver'; import { IElectronConfiguration, resolveElectronConfiguration } from './electron'; import { measureAndLog } from './logger'; +import { ChildProcess } from 'child_process'; -export async function launch(options: LaunchOptions): Promise<{ client: IDisposable; driver: IDriver }> { +export async function launch(options: LaunchOptions): Promise<{ electronProcess: ChildProcess; driver: PlaywrightDriver }> { // Resolve electron config and update const { electronPath, args, env } = await resolveElectronConfiguration(options); @@ -18,12 +18,11 @@ export async function launch(options: LaunchOptions): Promise<{ client: IDisposa // Launch electron via playwright const { electron, context, page } = await launchElectron({ electronPath, args, env }, options); + const electronProcess = electron.process(); return { - client: { - dispose: () => { /* there is no client to dispose for electron, teardown is triggered via exitApplication call */ } - }, - driver: new PlaywrightDriver(electron, context, page, undefined /* no server */, options) + electronProcess, + driver: new PlaywrightDriver(electron, context, page, undefined /* no server process */, options) }; } diff --git a/test/automation/src/processes.ts b/test/automation/src/processes.ts new file mode 100644 index 00000000000..17dcb79b32b --- /dev/null +++ b/test/automation/src/processes.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ChildProcess } from 'child_process'; +import { promisify } from 'util'; +import * as treekill from 'tree-kill'; +import { Logger } from './logger'; + +export async function teardown(p: ChildProcess, logger: Logger, retryCount = 3): Promise { + const pid = p.pid; + if (typeof pid !== 'number') { + return; + } + + let retries = 0; + while (retries < retryCount) { + retries++; + + try { + return await promisify(treekill)(pid); + } catch (error) { + try { + process.kill(pid, 0); // throws an exception if the process doesn't exist anymore + logger.log(`Error tearing down process (pid: ${pid}, attempt: ${retries}): ${error}`); + } catch (error) { + return; // Expected when process is gone + } + } + } + + logger.log(`Gave up tearing down process client after ${retries} attempts...`); +} diff --git a/test/automation/src/scm.ts b/test/automation/src/scm.ts index 60a33abb93f..7186fee17ca 100644 --- a/test/automation/src/scm.ts +++ b/test/automation/src/scm.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Viewlet } from './viewlet'; -import { IElement } from '../src/driver'; +import { IElement } from './driver'; import { findElement, findElements, Code } from './code'; const VIEWLET = 'div[id="workbench.view.scm"]'; diff --git a/test/automation/tools/copy-driver-definition.js b/test/automation/tools/copy-driver-definition.js index cf757ecbc38..36352008a47 100644 --- a/test/automation/tools/copy-driver-definition.js +++ b/test/automation/tools/copy-driver-definition.js @@ -21,34 +21,14 @@ contents = `/*------------------------------------------------------------------ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/** - * Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise, - * and others. This API makes no assumption about what promise library is being used which - * enables reusing existing code without migrating to a specific promise implementation. Still, - * we recommend the use of native promises which are available in this editor. - */ -interface Thenable { - /** - * Attaches callbacks for the resolution and/or rejection of the Promise. - * @param onfulfilled The callback to execute when the Promise is resolved. - * @param onrejected The callback to execute when the Promise is rejected. - * @returns A Promise for the completion of which ever callback is executed. - */ - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; -} - ${contents} - -export interface IDisposable { - dispose(): void; -} - -export function connect(outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }>; `; const srcPath = path.join(path.dirname(__dirname), 'src'); const outPath = path.join(path.dirname(__dirname), 'out'); +if (!fs.existsSync(outPath)) { + fs.mkdirSync(outPath); +} fs.writeFileSync(path.join(srcPath, 'driver.d.ts'), contents); fs.writeFileSync(path.join(outPath, 'driver.d.ts'), contents); diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index afa4e9fced4..be81bb17d4c 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -15,6 +15,7 @@ export function setup(logger: Logger) { it('starts with "DE" locale and verifies title and viewlets text is in German', async function () { const app = this.app as Application; + await app.workbench.extensions.openExtensionsViewlet(); await app.workbench.extensions.installExtension('ms-ceintl.vscode-language-pack-de', false); await app.restart({ extraArgs: ['--locale=DE'] }); diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index a1226638fc7..438ea351ff0 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -45,7 +45,6 @@ const opts = minimist(args, { 'remote', 'web', 'headless', - 'legacy', 'tracing' ], default: { @@ -56,7 +55,6 @@ const opts = minimist(args, { remote?: boolean; headless?: boolean; web?: boolean; - legacy?: boolean; tracing?: boolean; build?: string; 'stable-build'?: string; @@ -71,9 +69,9 @@ const logsRootPath = (() => { if (opts.web) { logsName = 'smoke-tests-browser'; } else if (opts.remote) { - logsName = opts.legacy ? 'smoke-tests-remote-legacy' : 'smoke-tests-remote'; + logsName = 'smoke-tests-remote'; } else { - logsName = opts.legacy ? 'smoke-tests-electron-legacy' : 'smoke-tests-electron'; + logsName = 'smoke-tests-electron'; } return path.join(logsParentPath, logsName); @@ -109,7 +107,7 @@ const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); if (fs.existsSync(testDataPath)) { rimraf.sync(testDataPath); } -fs.mkdirSync(testDataPath); +mkdirp.sync(testDataPath); process.once('exit', () => { try { rimraf.sync(testDataPath); @@ -326,13 +324,11 @@ before(async function () { workspacePath, userDataDir, extensionsPath, - waitTime: parseInt(opts['wait-time'] || '0') || 20, logger, logsPath: path.join(logsRootPath, 'suite_unknown'), verbose: opts.verbose, remote: opts.remote, web: opts.web, - legacy: opts.legacy, tracing: opts.tracing, headless: opts.headless, browser: opts.browser, @@ -366,7 +362,7 @@ after(async function () { } }); -describe(`VSCode Smoke Tests (${opts.web ? 'Web' : opts.legacy ? 'Electron (legacy)' : 'Electron'})`, () => { +describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web) { setupDataLossTests(() => opts['stable-build'] /* Do not change, deferred for a reason! */, logger); } setupPreferencesTests(logger); setupSearchTests(logger); diff --git a/test/smoke/src/utils.ts b/test/smoke/src/utils.ts index 1272924181b..049f20535d5 100644 --- a/test/smoke/src/utils.ts +++ b/test/smoke/src/utils.ts @@ -149,6 +149,27 @@ export function timeout(i: number) { }); } +export async function retryWithRestart(app: Application, testFn: () => Promise, retries = 3, timeoutMs = 20000): Promise { + let lastError: Error | undefined = undefined; + for (let i = 0; i < retries; i++) { + const result = await Promise.race([ + testFn().then(() => true, error => { + lastError = error; + return false; + }), + timeout(timeoutMs).then(() => false) + ]); + + if (result) { + return; + } + + await app.restart(); + } + + throw lastError ?? new Error('retryWithRestart failed with an unknown error'); +} + export interface ITask { (): T; } diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 7b2894db1e1..8e9646c0d65 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -12,11 +12,11 @@ const minimist = require('minimist'); const [, , ...args] = process.argv; const opts = minimist(args, { - boolean: ['web', 'legacy'], + boolean: ['web'], string: ['f', 'g'] }); -const suite = opts['web'] ? 'Browser Smoke Tests' : opts['legacy'] ? 'Desktop Smoke Tests (Legacy)' : 'Desktop Smoke Tests'; +const suite = opts['web'] ? 'Browser Smoke Tests' : 'Desktop Smoke Tests'; const options = { color: true, diff --git a/test/unit/node/index.js b/test/unit/node/index.js index 436eec5ed91..fd55cea9d93 100644 --- a/test/unit/node/index.js +++ b/test/unit/node/index.js @@ -25,11 +25,12 @@ const optimist = require('optimist') const TEST_GLOB = '**/test/**/*.test.js'; -const excludeGlob = '**/{browser,electron-sandbox,electron-browser,electron-main}/**/*.test.js'; -const excludeModules = [ - 'vs/platform/environment/test/node/nativeModules.test.js', // native modules are compiled against Electron and this test would fail with node.js - 'vs/base/parts/storage/test/node/storage.test.js', // same as above, due to direct dependency to sqlite native module - 'vs/workbench/contrib/testing/test/common/testResultService.test.js' // flaky (https://github.com/microsoft/vscode/issues/137853) + +const excludeGlobs = [ + '**/{browser,electron-sandbox,electron-browser,electron-main}/**/*.test.js', + '**/vs/platform/environment/test/node/nativeModules.test.js', // native modules are compiled against Electron and this test would fail with node.js + '**/vs/base/parts/storage/test/node/storage.test.js', // same as above, due to direct dependency to sqlite native module + '**/vs/workbench/contrib/testing/test/**' // flaky (https://github.com/microsoft/vscode/issues/137853) ]; /** @@ -152,7 +153,7 @@ function main() { /** @type {string[]} */ const modules = []; for (let file of files) { - if (!minimatch(file, excludeGlob) && excludeModules.indexOf(file) === -1) { + if (!excludeGlobs.some(excludeGlob => minimatch(file, excludeGlob))) { modules.push(file.replace(/\.js$/, '')); } } diff --git a/yarn.lock b/yarn.lock index b2929b3d0a8..63afc242b61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1046,10 +1046,10 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" -"@playwright/test@1.20.2": - version "1.20.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.20.2.tgz#0da1f24bf12d5a7249fa771a5344b76170f62653" - integrity sha512-unkLa+xe/lP7MVC0qpgadc9iSG1+LEyGBzlXhGS/vLGAJaSFs8DNfI89hNd5shHjWfNzb34JgPVnkRKCSNo5iw== +"@playwright/test@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.21.0.tgz#611dd3f469c539e5be3a764395effa40735742a6" + integrity sha512-jvgN3ZeAG6rw85z4u9Rc4uyj6qIaYlq2xrOtS7J2+CDYhzKOttab9ix9ELcvBOCHuQ6wgTfxfJYdh6DRZmQ9hg== dependencies: "@babel/code-frame" "7.16.7" "@babel/core" "7.16.12" @@ -1080,7 +1080,7 @@ ms "2.1.3" open "8.4.0" pirates "4.0.4" - playwright-core "1.20.2" + playwright-core "1.21.0" rimraf "3.0.2" source-map-support "0.4.18" stack-utils "2.0.5" @@ -2434,13 +2434,6 @@ async-settle@^1.0.0: dependencies: async-done "^1.2.2" -async@^2.1.5: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2980,7 +2973,7 @@ chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4303,10 +4296,10 @@ electron-to-chromium@^1.4.17: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.45.tgz#cf1144091d6683cbd45a231954a745f02fb24598" integrity sha512-czF9eYVuOmlY/vxyMQz2rGlNSjZpxNQYBe1gmQv7al171qOIhgyO9k7D5AKlgeTCSPKk+LHhj5ZyIdmEub9oNg== -electron@17.4.0: - version "17.4.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-17.4.0.tgz#9ad7b6bac92241dad5e8ab3e9f652f0ed3114859" - integrity sha512-eMuCOZMB9qsY63qzxEkyyqM09qs6mrbPBBDJJZgd8pnPWftE4zKmFp3B1vdHzjQ+1c1r/siigxbWTrpDNNri0A== +electron@17.4.1: + version "17.4.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-17.4.1.tgz#203961ca7551553d6497a1ec61d5f85d64d7e2ef" + integrity sha512-0qX+DbiNXlVSUxXq4lWVTis8QYqC4Q7R/Xkk3YZQbHMXZ90bWilypC3gBZAcN4MQD4AYUIebphBOpRPxlXY3nQ== dependencies: "@electron/get" "^1.13.0" "@types/node" "^14.6.2" @@ -5954,19 +5947,6 @@ gulp-replace@^0.5.4: readable-stream "^2.0.1" replacestream "^4.0.0" -gulp-shell@^0.6.5: - version "0.6.5" - resolved "https://registry.yarnpkg.com/gulp-shell/-/gulp-shell-0.6.5.tgz#f07b204ad8ad1c2659f7a1b6d76efa16d416a759" - integrity sha512-f3m1WcS0o2B72/PGj1Jbv9zYR9rynBh/EQJv64n01xQUo7j7anols0eww9GG/WtDTzGVQLrupVDYkifRFnj5Zg== - dependencies: - async "^2.1.5" - chalk "^2.3.0" - fancy-log "^1.3.2" - lodash "^4.17.4" - lodash.template "^4.4.0" - plugin-error "^0.1.2" - through2 "^2.0.3" - gulp-sourcemaps@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz#2e154e1a2efed033c0e48013969e6f30337b2743" @@ -7453,11 +7433,6 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= - lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -7493,27 +7468,12 @@ lodash.some@^4.2.2: resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" integrity sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= -lodash.template@^4.4.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4: +lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9002,10 +8962,10 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.20.2.tgz#02336afd9a631d59a666f11f3492550201c6c31b" - integrity sha512-iV6+HftSPalynkq0CYJala1vaTOq7+gU9BRfKCdM9bAxNq/lFLrwbluug2Wt5OoUwbMABcnTThIEm3/qUhCdJQ== +playwright-core@1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.21.0.tgz#1b68e87f4fd2fc5653def1e61ccdef6845c604a6" + integrity sha512-yDGVs9qaaW6WiefgR7wH1CGt9D6D/X4U3jNpIzH0FjjrrWLCOYQo78Tu3SkW8X+/kWlBpj49iWf3QNSxhYc12Q== dependencies: colors "1.4.0" commander "8.3.0" @@ -11439,16 +11399,16 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@4.7.0-dev.20220323: - version "4.7.0-dev.20220323" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.0-dev.20220323.tgz#eb9dd18f77a7fd58bbd53ce8d7dc86bdcda87e40" - integrity sha512-fJQwwIgPcAJV9qn45kgIaiiklW6eY+SuTVAM2WgNAJS7pGN6mfuUB7sk0G+HGTsWWI/gMlGVgNdPY3jK4M7Hfg== - typescript@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= +typescript@^4.7.0-dev.20220419: + version "4.7.0-dev.20220419" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.0-dev.20220419.tgz#c9bc57a9f4e79ee687ac60da6a2170bb3b482467" + integrity sha512-eqezQkTMfRDsbYTW3vWImen8yasP99iykbSLr1BjF7jusxmnvfOVWzXr1zsf+0+FWufBLwpa0rpZBd8TZHWmxg== + typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" @@ -12244,10 +12204,10 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" -xterm-addon-search@0.9.0-beta.18: - version "0.9.0-beta.18" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.18.tgz#5317aed1dc747f468ccb7ecd151fb00d82a8a19d" - integrity sha512-SAeA3thc2WJNYXwjOEJFLpZ1ZVOs22RLmz9a6WcrzXkvCjLZRvbRGwX25Ms+Dd7dVDQNbKVUzUJohspP/vYr0Q== +xterm-addon-search@0.9.0-beta.22: + version "0.9.0-beta.22" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.9.0-beta.22.tgz#18f2eabda82709cdd64dee003879b586869e28a3" + integrity sha512-1I8W0bwjg6bUoNaESKHSkS9BN3Z9Xe44/zbUb6Un+pbb5Nun4LQvFwSLkAIdRrslxcbcHYIU/2Q7F1wCOWLbaA== xterm-addon-serialize@0.7.0-beta.12: version "0.7.0-beta.12" @@ -12259,20 +12219,20 @@ xterm-addon-unicode11@0.4.0-beta.3: resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.4.0-beta.3.tgz#f350184155fafd5ad0d6fbf31d13e6ca7dea1efa" integrity sha512-FryZAVwbUjKTmwXnm1trch/2XO60F5JsDvOkZhzobV1hm10sFLVuZpFyHXiUx7TFeeFsvNP+S77LAtWoeT5z+Q== -xterm-addon-webgl@0.12.0-beta.27: - version "0.12.0-beta.27" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.27.tgz#afc5bc01d1ef3af9005fb9f6325a4db9c92aa8d9" - integrity sha512-P948trotU8FMHtaA7C2x97VpLq6QLSjO53kWNvONS0/XwEKQBIYCI7Jfri2wcLgfQg6Cn4OQGLoj2YBK3MMyww== +xterm-addon-webgl@0.12.0-beta.29: + version "0.12.0-beta.29" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.12.0-beta.29.tgz#7a508595c4521d14d7ed4315a121f9e3f230a0f0" + integrity sha512-NcZBsD0ar3ZpQX070hDIsyEBl/StRMNu6U+9crNpiD2rQVfkM1vcWkOv31Zlj3eu6/f8z5aStyZLRMCGFwiRbA== -xterm-headless@4.19.0-beta.20: - version "4.19.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.20.tgz#9e401920fcc24c2474e0bd45df932c62413594da" - integrity sha512-twp0vCyfdI4wVgDrwxaHk1FtC4UhTNNgbIPT6yVPjICOUkUTOvFjrQCNKHv2uMOJo9uAH2gyOsIqHdEP549rJA== +xterm-headless@4.19.0-beta.25: + version "4.19.0-beta.25" + resolved "https://registry.yarnpkg.com/xterm-headless/-/xterm-headless-4.19.0-beta.25.tgz#a0a1b59f386c44458f06b8ced64e3567371cc983" + integrity sha512-UswSgymk3g9i6XTpFAasnqqIvWhi+AEWT+iO3kkjII6ll+dYEQgeZAv92EnCmeRHp11u5TP+IBAo8jy+aTYbtA== -xterm@4.19.0-beta.20: - version "4.19.0-beta.20" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.20.tgz#d8e970d8a8460c1d1a5ec9866f78f607a44c1349" - integrity sha512-IYI4ngSWzpV4sJXLWGEDF7vgLuUHn0CUQ42+TGv4H/hCGo4uru4s/D3Yws0ETb3a9VwRpZEPsigULaWTnhFusg== +xterm@4.19.0-beta.25: + version "4.19.0-beta.25" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.19.0-beta.25.tgz#38f92d0fef1cfdb290ef8994449a04fa1a8c90a7" + integrity sha512-pDiMWKN1Cj4+X/K9Xegp0SA0ZDEGVqiq7RPSy8oZO2wo2rze1BF20PAZb3/RSp30eY5WyOKilKnck4yNOsPzHw== y18n@^3.2.1: version "3.2.2"